为什么这个C风格的转换没有考虑static_cast后跟const_cast?

17

请考虑:

float const& f = 5.9e-44f;
int const i = (int&) f;

根据expr.cast/4,应该按顺序考虑以下内容:
- 一个const_­cast - 一个static_­cast - 一个static_­cast后跟一个const_­cast - 一个reinterpret_­cast - 一个reinterpret_­cast后跟一个const_­cast 显然,static_­cast<int const&>后跟const_­cast<int&>可行的,将导致值为0int。但是所有编译器都会将i初始化为42,这表明它们选择了最后一个选项reinterpret_­cast<int const&>后跟const_­cast<int&>。为什么?
相关问题:在C++中,C样式转换可以调用转换函数,然后取消const限定吗?, 为什么(int&)0不合法?, C++规范是否说明如何选择在静态转换/常量转换链中使用的类型来执行C样式转换?, (float&)int的类型切换有效,而(float const&)int的类型切换类似于(float)int吗?

1
float 转换为 int 不等同于将对 float 的引用转换为对 int 的引用。无论引用是否带有 const 限定符都是不相关的。 - Sam Varshavchik
5
“const”是一个误导性的东西,会分散注意力而不是解决真正的问题。请考虑这个简化的完整示例:https://godbolt.org/z/oaxz31j99 - François Andrieux
@FrançoisAndrieux const 是必要的,以允许 static_cast 链条正常工作;你需要构造一个临时变量。 - ecatmur
@ecatmur 我想我理解了。你希望对于一个 float f;(int&) 使用 reinterpret_cast 进行转换,但是对于一个 const float & f,希望使用 static_cast + const_cast 进行转换,因为在第二种情况下,f 是一个引用类型? - François Andrieux
1
哎呀,我的意思是这个问题也与此相关,尽管这里的答案更好,而且可能同样适用:https://dev59.com/WFUL5IYBdhLWcg3w670R - user17732522
显示剩余6条评论
2个回答

6

简述:

  • const_cast<int&>(static_cast<int const&>(f)) 是合法的c++代码
  • (int&)f 应该得到相同的结果
  • 但是由于一个古老的编译器bug从未被修复,所以它不能得到相同的结果

长说明

1. 为什么const_cast<int&>(static_cast<int const&>(f))有效

1.1 static_cast

让我们从static_cast<int const&>(f)开始:

  • 让我们来看看转换的结果是什么:
    7.6.1.9 静态转换(重点在于我)

    (1) 表达式 static_­cast<T>(v) 的结果是将表达式 v 转换为类型 T 的结果。如果 T左值引用类型或函数类型的右值引用,则结果为左值;如果 T 是对象类型的右值引用,则结果为 xvalue;否则,结果为 prvalue。static_­cast 运算符不得消除 constness(expr.const.cast)。

    int const& 是左值引用类型,因此 static_cast<>() 的结果必须是某种左值。

  • 然后让我们找出实际发生的转换:
    7.6.1.9 静态转换

    (4) 如果从 ET 存在隐式转换序列(over.best.ics),则可以将表达式 E 显式转换为类型 T。[...]
    如果 T 是引用类型,则效果与执行声明和初始化
    T t(E);
    对于某些虚构的临时变量 t([dcl.init]),然后使用临时变量作为转换的结果相同。

    • 在我们的情况下,声明如下:
      const int& t(f);
    • 我不打算在这里详细阐述整个转换过程,以保持简洁,您可以在 12.2.4.2 隐式转换序列 中阅读确切的细节。
    • 在我们的情况下,转换序列将由 2 步组成:
      • 将 glvalue float 转换为 prvalue(这也允许我们摆脱 const
        7.3.2 左值到右值的转换(重点在于我)

        (1) 非函数、非数组类型 T 的 glvalue 可以转换为 prvalue。如果 T 是不完整类型,则需要进行此转换的程序是有错误的。如果 T非类类型,则 prvalue 的类型是T 的 cv 限定版本。否则,prvalue 的类型是 T

        鉴于 float 是非类类型,这允许我们将 ffloat const& 转换为 float&&

      • 从 float 转换为 int
        7.3.11 浮点-整数转换

        (1) 浮点类型的 prvalue 可以转换为整数类型的 prvalue。转换截断;也就是说,小数部分被舍去。如果截断后的值不能在目标类型中表示,则行为未定义。

        因此,我们最终得到了一个漂亮的从 f 转换来的 int 值。1.2 const_cast

        现在我们知道了static_cast<>部分的返回值,我们可以专注于const_cast<int&>()

        • 结果类型需要为:
          7.6.1.11 const_cast(我强调)

          (1) 表达式 const_­cast<T>(v) 的结果类型为 T。如果 T对象类型的左值引用,则 结果是左值;如果 T 是对象类型的右值引用,则结果是 xvalue;否则,结果是 prvalue,并对表达式 v 执行 lvalue-to-rvalue、array-to-pointer 和 function-to-pointer 标准转换。可以使用 const_­cast 显式地执行的转换如下所示。不得使用 const_­cast 显式地执行任何其他转换。

          static_cast<> 的结果是左值,因此 const_cast<> 的结果也必须是左值。

        • const_cast<> 执行什么转换? 7.6.1.11 const_cast(我强调)

          (4) 对于两个对象类型 T1T2,如果可以使用 const_­cast 将指向 T1 的指针显式转换为类型“指向 T2 的指针”,则还可以进行以下转换:
          (4.1) 使用强制类型转换 const_­cast<T2&>将类型为 T1 的左值显式转换为类型为 T2 的左值
          (4.2) 使用强制类型转换 const_­cast<T2&&>,将类型为 T1 的 glvalue 显式转换为类型为 T2 的 xvalue;以及
          (4.3) 如果 T1 是类类型,则可以使用强制类型转换 const_­cast<T2&&> 将类型为 T1 的 prvalue 显式转换为类型为 T2 的 xvalue。

          如果操作数是 glvalue,则 reference const_­cast 的结果引用原始对象;否则引用应用临时材料化的结果。

          const_cast<> 将 lvalue const int& 转换为 int& 左值,它将引用同一对象。

        1.3 结论

        const_cast<int&>(static_cast<int const&>(f)) 是合法的,并将得到一个左值 int 引用。

        你甚至可以按照 6.7.7 临时对象 的规定延长引用的生命周期。

        (6) 如果引用所绑定的glvalue是通过以下方式之一获得的,则引用所绑定的临时对象或作为其子对象完整对象的临时对象将在引用的生命周期内保持存在:
        [...]
        - (6.6) a
        - (6.6.1) const_cast (expr.const.cast),
        [...]
        将一个glvalue操作数转换为一个引用到指定操作数所指定的对象、其完整对象或其子对象的glvalue,而不需要用户定义的转换,
        [...]

        所以这也是合法的:

        float const& f = 1.2f; 
        int& i = const_cast<int&>(static_cast<int const&>(f));
        
        i++; // legal
        return i; // legal, result: 2
        
        1.4笔记
        • It is irrelevant in this case that the operand of static_cast<> is a const float reference, since the lvalue-to-rvalue conversion that static_cast is allowed to perform can strip away const.
          So those would also be legal:
          int& i = const_cast<int&>(static_cast<int const&>(1.0f));
          // when converting to rvalue you don't even need a const_cast:
          // (due to 7.6.1.9 (4), because int&& t(1.0f); is well-formed)
          // the result of the static_cast would be an xvalue in this case. 
          int&& ii = static_cast<int&&>(1.0f);
          
        • Because of that the following c-style casts are also well-formed:
          float f = 1.2f;
          int const& i = (int const&)f; // legal, will use static_cast
          int&& ii = (int&&)f; // legal, will use static_cast
          

        2. 为什么 (int&)f 不起作用

        从技术上讲,它应该起作用,因为C风格的转换允许执行此转换序列:

        7.6.3 显式类型转换(强制类型转换)

        (4) 通过以下方式进行的转换:
        (4.1) const_­cast (expr.const.cast),
        (4.2) static_­cast (expr.static.cast),
        (4.3) static_­cast 后跟 const_­cast
        (4.4) reinterpret_­cast (expr.reinterpret.cast),或
        (4.5) reinterpret_­cast 后跟 const_­cast
        可以使用显式类型转换的强制类型转换符号进行。同样的语义限制和行为适用于这些转换,[...]。

        所以const_cast<int&>(static_cast<int const&>(f))应该是一个有效的转换序列。

        这不起作用的原因实际上是一个非常古老的编译器错误。

        2.1 这甚至是一个open-std.org问题(#909)

        According to 7.6.3 [expr.cast] paragraph 4, one possible interpretation of an old-style cast is as a static_cast followed by a const_cast. One would therefore expect that the expressions marked #1 and #2 in the following example would have the same validity and meaning:

        struct S {
          operator const int* ();
        };
        
        void f(S& s)  {
          const_cast<int*>(static_cast<const int*>(s));  // #1
          (int*) s;  // #2
        }
        

        However, a number of implementations issue an error on #2.

        Is the intent that (T*)x should be interpreted as something like const_cast<T*>(static_cast<const volatile T*>(x))

        结果如下:

        理由(2009年7月): 根据文字的直接解释,该示例应该有效。这似乎只是编译器的错误。

        因此,标准与您的结论相符,只是没有编译器实现该解释。

        2.2 编译器错误票证

        已经有关于gcc和clang的此问题的已打开bug:

        2.3 为什么这些年后还没有解决?

        我不知道,但考虑到他们现在必须每3年左右实现一个新的标准,并且每次对语言进行大量更改,似乎有理由忽略大多数程序员可能永远不会遇到的问题。

        请注意,这只是原始类型的问题。我猜测出现这个错误的原因是因为对于那些类型,由于左值到右值的转换规则,可以通过static_cast / reinterpret_cast来删除cv限定符。

        如果T是非类类型,则prvalue的类型是T的无cv限定符版本。否则,prvalue的类型为T。

        请注意,此错误仅影响非类类型,对于类类型,它将完美地工作:
        struct B { int i; };
        struct D : B {};
        
        D d;
        d.i = 12;
        B const& ref = d;
        
        // works
        D& k = (D&)ref;
        

        每个编译器都会存在一些未正确实现的边缘情况,如果这让你感到困扰,你可以提供修复方法,也许它们会在下一个版本中合并(至少适用于clang和gcc)。

        2.4 gcc代码分析

        在gcc中,C风格的转换目前通过cp_build_c_cast解决:

        tree cp_build_c_cast(location_t loc, tree type, tree expr, tsubst_flags_t complain) {
          tree value = expr;
          tree result;
          bool valid_p;
          // [...]
          /* A C-style cast can be a const_cast.  */
          result = build_const_cast_1 (loc, type, value, complain & tf_warning,
                           &valid_p);
          if (valid_p)
            {
              if (result != error_mark_node)
            {
              maybe_warn_about_useless_cast (loc, type, value, complain);
              maybe_warn_about_cast_ignoring_quals (loc, type, complain);
            }
              return result;
            }
        
          /* Or a static cast.  */
          result = build_static_cast_1 (loc, type, value, /*c_cast_p=*/true,
                        &valid_p, complain);
          /* Or a reinterpret_cast.  */
          if (!valid_p)
            result = build_reinterpret_cast_1 (loc, type, value, /*c_cast_p=*/true,
                               &valid_p, complain);
          /* The static_cast or reinterpret_cast may be followed by a
             const_cast.  */
          if (valid_p
              /* A valid cast may result in errors if, for example, a
             conversion to an ambiguous base class is required.  */
              && !error_operand_p (result))
          {
            tree result_type;
        
            maybe_warn_about_useless_cast (loc, type, value, complain);
            maybe_warn_about_cast_ignoring_quals (loc, type, complain);
        
            /* Non-class rvalues always have cv-unqualified type.  */
            if (!CLASS_TYPE_P (type))
              type = TYPE_MAIN_VARIANT (type);
            result_type = TREE_TYPE (result);
        
            if (!CLASS_TYPE_P (result_type) && !TYPE_REF_P (type))
              result_type = TYPE_MAIN_VARIANT (result_type);
        
            /* If the type of RESULT does not match TYPE, perform a
              const_cast to make it match.  If the static_cast or
              reinterpret_cast succeeded, we will differ by at most
              cv-qualification, so the follow-on const_cast is guaranteed
              to succeed.  */
            if (!same_type_p (non_reference (type), non_reference (result_type)))
            {
              result = build_const_cast_1 (loc, type, result, false, &valid_p);
              gcc_assert (valid_p);
            }
        
            return result;
          }
        
          return error_mark_node;
        }
        

        实现基本上是这样的:
        • 尝试使用 const_cast
        • 尝试使用 static_cast(在暂时忽略潜在的 const 不匹配的情况下)
        • 尝试使用 reinterpret_cast(在暂时忽略潜在的 const 不匹配的情况下)
        • 如果在 static_castreinterpret_cast 变体中存在 const 不匹配,则在其前面添加 const_cast
        所以由于某种原因,在这种情况下 build_static_cast_1 没有成功,因此 build_reinterpret_cast_1 执行了它的任务(这将导致由于严格别名规则而产生未定义行为)。

1
惊人的。谢谢你。 - viraltaco_
1
非常好的分析,谢谢!看了你指出的代码后,我认为通过/操作c_cast_p应该可以解决我的问题和相关的CWG 909?类似这样:https://github.com/gcc-mirror/gcc/compare/master...ecatmur:so-66816741 - ecatmur
@ecatmur,你修复了它!太棒了 :D 可惜我对gcc代码库还不是很熟悉。我编译了你的修复程序并运行了测试,除了constexpr-union.C中的第16行(在constexpr上下文中不允许使用reinterpret_cast<>)之外,其他都正常工作。但除此之外看起来很好 :) - Turtlefight

-1

这可能是未定义的行为。但是,为了尝试回答这个问题,据我所知:

您取消了const,然后将其重新解释为int&(**)
不是static_cast吗?
它已经是一个引用,指向一个不是pointer-interconvertibleint&的lvalue。(*)

那个reinterpret_cast(?)的结果将是未定义的行为;它将违反strict aliasing rule

您可以通过使用std::is_pointer_interconvertible_base_of_v<>来在尝试之前检查。请参见:cppreference.com

如果我们忽略const,它仍然不讲得通。
我读得越多,就越不确定任何事情。这就是为什么我们告诉你不要使用c风格的转换。

注释(*):那是错的,还是对的?有多种方法可以完成此转换...
(**):不是那样的... 我不知道我在说什么...


你将const强制转换掉,然后reinterpret_cast它作为int&。但根据C++标准,C风格的强制转换执行的是reinterpret_cast,接着是const_cast,而不是相反。只有在static_cast后跟const_cast不可行时才会这样做;但正如所示,这种情况下使用static_cast后跟const_cast是可行的。 - ecatmur
可以隐式地添加const。如果要删除它,则必须显式声明。[expr.static.cast] - viraltaco_
实际上,只需阅读整个章节[expr.cast](就像我昨天做的那样,已经读了5遍),我太累了,无法阅读这个小字体。 需要注意的是,“如果一个转换可以被解释为static_­cast后跟const_­cast的多种方式,则该转换是非法的。” - viraltaco_
2
好的,那么有什么备选转换路径呢?此外,如果它格式不正确(注意,不是格式不正确的 NDR),那么不应该拒绝它吗? - ecatmur
“但根据C++标准,C风格的转换执行的是reinterpret_cast后跟const_cast,而不是相反。”你刚才让我困惑了,这是指:reinterpret_cast<new-type>(const_cast<new-type>(expression)) 还是相反的顺序? - user15676138
@cpper 不是这样,内部转换先发生(跟随)外部转换。 - ecatmur

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接