【问题标题】:Why does this C-style cast not consider static_cast followed by const_cast?为什么这种 C 风格的转换不考虑 static_cast 后跟 const_cast?
【发布时间】:2021-06-23 06:29:47
【问题描述】:

考虑:

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&>viable,并且会产生一个值为 0int。但是所有编译器都将i初始化为42,这表明他们采用了reinterpret_­cast<int const&>的最后一个选项,然后是const_­cast<int&>。为什么?

相关:In C++, can a C-style cast invoke a conversion function and then cast away constness?Why is (int&)0 ill-formed?Does the C++ specification say how types are chosen in the static_cast/const_cast chain to be used in a C-style cast?Type punning with (float&)int works, (float const&)int converts like (float)int instead?

【问题讨论】:

  • float 转换为int 与将对float 的引用转换为对int 的引用不同。引用是否为 const 限定并不重要。
  • const 是一个红鲱鱼,分散了真正问题的注意力。考虑这个简化的完整示例:godbolt.org/z/oaxz31j99
  • 我认为这与不允许将非 const 的左值引用绑定到纯右值有关。
  • @FrançoisAndrieux const 是允许static_cast 链工作所必需的;你需要建造一个临时的。
  • @ecatmur 我想我明白了。你会期望 (int&)reinterpret_cast 对于 float f; 但期望 static_cast + const_cast 对于 const float & f 因为在第二种情况下 f 是引用类型?

标签: c++ casting language-lawyer reinterpret-cast


【解决方案1】:

tl;博士:

  • const_cast<int&>(static_cast<int const&>(f)) 是有效的 c++
  • (int&)f 应该有同样的结果
  • 但这不是由于一个从未修复过的古老编译器错误

详细说明

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

1.1 static_cast

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

  • 让我们看看这个演员的结果是什么:
    7.6.1.9 Static cast(强调我的)

    (1) 表达式static_­cast<T>(v) 的结果是将表达式v 转换为类型T 的结果。如果T左值引用类型 或对函数类型的右值引用,则结果为左值;如果T 是对对象类型的右值引用,则结果是一个xvalue;否则,结果为纯右值。 static_cast 运算符不应抛弃常量 (expr.const.cast)。

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

  • 那么让我们看看实际发生了什么转换:
    7.6.1.9 Static cast

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

    • 在我们的例子中,声明如下所示:
      const int& t(f);
    • 为了简短起见,我不打算在这里详细说明整个转换过程,您可以阅读12.2.4.2 Implicit conversion sequences的确切细节
    • 在我们的例子中,转换序列包括 2 个步骤:
      • 将 glvalue 浮点数转换为纯右值(这也使我们能够摆脱 const
        7.3.2 Lvalue-to-rvalue conversion(强调我的)

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

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

      • 从浮点数转换为整数
        7.3.11 Floating-integral conversions

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

        所以我们最终得到了一个从 f 很好地转换的 int 值。

  • 所以static_cast<> 部分的最终结果是一个左值int const&

1.2 const_cast

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

  • 结果类型需要是:
    7.6.1.11 Const cast(强调我的)

    (1) 表达式const_­cast<T>(v) 的结果是T 类型。如果T对对象类型的左值引用,则结果是左值;如果T 是对对象类型的右值引用,则结果是一个xvalue;否则,结果是纯右值,并且左值到右值、数组到指针和函数到指针的标准转换在表达式v 上执行。下面列出了可以使用 const_cast 显式执行的转换。不应使用 const_cast 显式执行其他转换。

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

  • const_cast<> 做了什么转换? 7.6.1.11 Const cast(强调我的)

    (4) 对于T1T2这两种对象类型,如果指向T1的指针可以使用const_cast显式转换为“指向T2的指针”类型,那么也可以进行以下转换:
    (4.1)左值类型 T1 可以显式转换为左值类型 T2 使用强制转换 const_­cast<T2&>;
    (4.2)T1 类型的左值可以显式转换为 @987654397 类型的 xvalue @使用演员const_­cast<T2&&>;并且
    (4.3) 如果T1 是类类型,则T1 类型的纯右值可以使用强制转换const_­cast<T2&&> 显式转换为T2 类型的xvalue。
    如果操作数是泛左值,则引用 const_cast 的结果引用原始对象,否则引用应用临时实现转换的结果。

    所以const_cast<> 会将左值const int& 转换为int& 左值,这将指向同一个对象。

1.3结论

const_cast<int&>(static_cast<int const&>(f)) 格式正确,会产生一个左值 int 引用。

您甚至可以按照6.7.7 Temporary objects 延长引用的生命周期

(6) 引用绑定的临时对象或作为引用绑定的子对象的完整对象的临时对象在引用的生命周期内持续存在,如果glvalue通过以下方式之一获得引用所绑定的:
[...]
- (6.6) 一个
- (6.6.1)const_cast (expr.const.cast),
[...]
在没有用户定义的转换的情况下,将作为这些表达式之一的泛左值操作数转换为指代由操作数指定的对象或其完整对象或其子对象的泛左值,
[...]

所以这也是合法的:

float const& f = 1.2f; 
int& i = const_cast<int&>(static_cast<int const&>(f));

i++; // legal
return i; // legal, result: 2
1.4 注释
  • 在这种情况下,static_cast&lt;&gt; 的操作数是 const 浮点引用无关紧要,因为允许 static_cast 执行的左值到右值转换可以剥离 const。
    所以这些也是合法的:
    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);
    
  • 因此,以下 c 样式转换也是格式正确的:
    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&amp;)f 不起作用

您在技术上是正确的,因为它应该可以工作,因为允许 c 样式转换执行此转换序列:

7.6.3 Explicit type conversion (cast notation)

(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&lt;int&amp;&gt;(static_cast&lt;int const&amp;&gt;(f)) 绝对应该是一个有效的转换序列。

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

2.1 甚至是open-std.org issue (#909):

根据 7.6.3 [expr.cast] 第 4 段,对旧式强制转换的一种可能解释是 static_cast 后跟 const_cast。因此,人们会期望以下示例中标记为 #1 和 #2 的表达式具有相同的有效性和含义:

struct S {
  operator const int* ();
};

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

但是,许多实现在 #2 上发出错误。

(T*)x 是否应该被解释为 const_cast&lt;T*&gt;(static_cast&lt;const volatile T*&gt;(x)) 之类的东西

结果是:

基本原理(2009 年 7 月): 根据措辞的直接解释,该示例应该有效。这似乎只是一个编译器错误。

所以标准同意你的结论,只是没有编译器真正实现了这种解释。

2.2 编译器错误票

gcc 和 clang 已经有关于这个问题的开放错误:

2.3 为什么这么多年还没有修复?

我不知道,但考虑到他们现在大约每 3 年就必须实施一项新标准,每次都会对语言进行大量更改,因此忽略大多数程序员可能永远不会遇到的问题似乎是合理的。

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

如果T是非类类型,则prvalue的类型是T的cv-unqualified version。否则,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 开始做这件事(由于严格的别名规则,这将导致未定义的行为)

【讨论】:

  • 太棒了。谢谢。
  • 很好的分析,谢谢!查看您指出的代码,我认为通过/执行c_cast_p 应该可以解决我的问题和相关的 CWG 909?比如:github.com/gcc-mirror/gcc/compare/master...ecatmur:so-66816741
  • @ecatmur 你做了修复!太棒了 :D 不幸的是,我对 gcc 代码库还不是很熟悉。我编译了您的修复程序并运行了测试,除了constexpr-union.C 一行 16(reinterpret_cast&lt;&gt; 不允许在constexpr 上下文中)之外,它们都有效。但除此之外它看起来不错:)
【解决方案2】:

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

您将const 扔掉,然后将reinterpret_cast 转换为int&amp; (**)
这不是static_cast
它已经是对不是 pointer-interconvertibleint&amp; 的左值的引用。 (*)

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

您可以在尝试之前使用std::is_pointer_interconvertible_base_of_v&lt;&gt; 进行检查。见:cppreference.com

如果我们忽略 const仍然没有有意义。
我读得越多,我就越不确定。这就是为什么我们告诉您不要使用 c 样式强制转换。

注释 (*):这是错误的,是吗?不止一种方法可以给这个演员表蒙皮……
(**):不是这样的……我不知道我在说什么……

【讨论】:

  • "你把 const 去掉,然后将它重新解释为 int&。"但是根据 C++ 标准,C 风格的转换执行 reinterpret_cast 后跟 const_cast,而不是相反。并且只有在 static_cast 后跟 const_cast 不可行的情况下;但在这种情况下它是可行的,正如所证明的那样。
  • 您可以隐式添加const。删除它,必须是明确的。 [expr.static.cast]
  • 其实,只要阅读整章[expr.cast] (就像我昨天一样,5 次)我太累了,看不懂这个小字体。值得注意的是“如果转换可以以多种方式解释为 static_cast 后跟 const_cast,则该转换是格式错误的。”
  • 好的,那么替代转化路径是什么?另外,如果它是格式错误的(注意,不是格式错误的 NDR)那么不应该被拒绝吗?
猜你喜欢
  • 2010-12-09
  • 2010-09-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-03-19
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多