这种类型转换是否定义良好?

4
这个答案中,有一段关于严格别名规则的引用,针对C++11规范,内容如下:

如果程序试图通过除以下类型之一的glvalue来访问对象的存储值,则行为是未定义的:

  • ...

  • 包括上述类型之一在其元素或非静态数据成员中的聚合体或联合体类型(包括,递归地,子聚合体或包含联合体的元素或非静态数据成员),

  • ...

所以我认为下面的代码不违反严格别名规则:
#include <iostream>
#include <cstdint>
#include <climits>
#include <limits>

struct PunnerToUInt32
{
    std::uint32_t ui32;
    float fl;
};

int main()
{
    static_assert(std::numeric_limits<float>::is_iec559 &&
                  sizeof(float)==4 && CHAR_BIT==8,"Oops");
    float x;
    std::uint32_t* p_x_as_uint32=&reinterpret_cast<PunnerToUInt32*>(&x)->ui32;
    *p_x_as_uint32=5;
    std::cout << x << "\n";
}

好的,严格别名规则得到满足。但是还有其他原因导致未定义行为吗?

3
如何满足严格别名规则?只要两种类型都不是获得免费通行证的char*类型即可。请注意,翻译后的内容应保持原意和准确性,同时更易于理解。 - Richard Hodges
1
@RichardHodges 你比我快。 - Jonathan Mee
@JonathanMee 有些错误是不能被忽视的。你从我这里得到了+1。 - Richard Hodges
2
经验法则:如果你看到 reinterpret_cast,并且它不是用于 char*,那么在99.9%的情况下,你正在面对严格别名违规问题(0.1%的情况是保留给指针实际上没有被解引用,而是用于另一种允许的转换的情况)。 - SergeyA
2个回答

4
您不能这样做:&reinterpret_cast<PunnerToUInt32*>(&x) 有关reinterpret_cast的规则如下:
当将动态类型为“DynamicType”的对象的指针或引用被reinterpret_cast(或C风格转换)为指向不同类型“AliasedType”的对象的指针或引用时,强制转换总是成功的,但生成的指针或引用只能用于访问对象,如果以下情况之一成立:
- AliasedType是(可能带有CV限定符的)DynamicType。 - AliasedType和DynamicType都是指向相同类型T的(可能是多级的,每个级别可能带有CV限定符)指针。 - AliasedType是(可能带有CV限定符的)DynamicType的有符号或无符号变体。 - AliasedType是聚合类型或联合类型,其作为元素或非静态成员包含上述类型之一(包括递归地包含子聚合的元素和包含的联合的非静态数据成员):这使得通过指向其非静态成员或元素的指针可以安全地获得指向结构体或联合体的可用指针。 - AliasedType是DynamicType的(可能带有CV限定符的)基类。 - AliasedType是char或unsigned char:这允许将任何对象的对象表示作为unsigned char数组进行检查。
因为对于DynamicType为float且AliasedType为PunnerToUInt32的组合,这些条件都不成立,所以指针不能用于访问对象,而您正在这样做。这使得行为未定义。
有关更多信息,请参见:为什么reinterpret_cast在相同大小的类型之间强制执行copy_n转换? 编辑:
将第四个项目分解为小块:
  1. "AliasedType"
    这里指的是PunnerToUInt32
  2. "是一个聚合类型或联合类型"
    PunnerToUInt32符合聚合类型的资格:

    • 数组类型
    • 类类型(通常是structunion),其具有
      • 没有私有或受保护的非静态数据成员
      • 没有用户提供的构造函数,包括从公共基类继承的那些(显式默认或删除的构造函数是允许的)
      • 没有虚拟、私有或受保护的基类
      • 没有虚成员函数
  3. "它作为元素或非静态成员持有前述类型之一(包括子聚合物的元素和所包含联合体的非静态数据成员,递归地)"
    同样,PunnerToUInt32由于其float fl成员而符合条件

  4. "这使得获取结构体或联合体的可用指针变得安全"
    这是正确的最后一部分,因为AliassedType是一个PunnerToUInt32
  5. "给定其非静态成员或元素的指针"
    这是一种违规行为,因为DynamicTypex不是PunnerToUInt32的成员

由于第5部分的违规操作,使用此指针的行为是未定义的。

如果您想阅读一些推荐的文章,可以查看Empty Base Optimization,如果不需要,我将为您提供主要相关信息:

为了维持指向标准布局对象的指针(使用reinterpret_cast转换)指向其初始成员的要求,需要进行空基类优化。

因此,您可以利用reinterpret_cast的第四个项目来执行以下操作:

PunnerToUInt32 x = {13, 42.0F};
auto y = reinterpret_cast<PunnerToUInt32*>(&x.ui32);

实时示例


1
但是,AliasedType=PunnedToUInt32不是一个聚合类型吗?它将DynamicType=float作为非静态成员持有,正如第4条所描述的那样,因此消除了违规吗? - Ruslan
@Ruslan 对不起,我第一次回答你的问题做得不太好。直到看到你的评论,我才明白你的问题所在。我已经编辑过了,希望你现在能看得明白了。如果还是有疑问,请告诉我怎么解释比较清楚。 - Jonathan Mee
你是否查看了实际标准,而不仅仅是在cppreference.com上查看?至少在我手头的C++14标准中,“这使得可以安全地获取指向结构体或联合体的可用指针,即使只有它的非静态成员或元素的指针。”部分缺失。我正在查看标准中的3.10.10节。 - khuttun
3.10 左值和右值。如果程序试图访问对象的存储值... - khuttun
1
我也在cppreference上开始了一场关于这个问题的讨论:http://en.cppreference.com/w/Talk:cpp/language/reinterpret_cast - khuttun
显示剩余5条评论

2
如果 p_x_as_uint32 指向 x,那么通过类型为 uint32_t 的 glvalue 访问类型为 float 的对象,会导致未定义行为,比如执行 *p_x_as_uint32=5

问题在于赋值操作中的“访问”,重要的是所使用的 glvalue 类型(uint32_t)和所访问的对象实际类型(float)。用于获取指针的一系列转换过程是不相关的。

值得记住的是,严格别名规则存在是为了启用基于类型的别名分析。无论经过多么痛苦的路径,如果你可以合法地“创建这样一种情况,即一个 int* 和一个 float* 可以同时存在,并且两者都可以用于加载或存储同一内存,那么你就破坏了 TBAA 规则。如果你认为标准的措辞允许你这样做,那么你可能是错误的,但如果你是正确的,那么你发现的只是标准措辞中的缺陷。


类成员访问是未定义行为(通过省略),因为没有实际的 PunnerToUInt32 对象。然而,类成员访问不是严格别名规则中的"访问",后者意味着“读取或修改对象的值”。

给出三个选择:(1)禁止编译器假定不会发生别名,即使没有证据表明可能会发生;(2)只允许编译器在没有证据表明可能会发生别名的地方假定不会发生别名;(3)鼓励编译器假定不会发生别名,即使有强有力的证据表明会发生别名。哪个选项最有意义?我建议规则得到批准的唯一原因是人们期望编译器编写者认识到第三个选项是如此愚蠢,以至于他们仅将规则解释为允许第二个选项。 - supercat
如果意思是第三种,并且作者不想故意使语言比没有规则时更弱,他们将添加足够的指令,以允许程序员做任何他们可以在没有规则的情况下做的事情(例如,“放置新”的变体,重新解释所指示存储器中可能包含的任何位模式)。如果编译器将指针重新解释识别为其直接区域内别名的证据,则编译器可以允许有用的指针重新解释而无需阻止所有优化。我不明白为什么这样很难。 - supercat
作为一个简单的原则:在代码没有做任何奇怪的事情的地方积极优化,但在代码做奇怪的事情时要非常谨慎,做出很少的假设。一个在代码做奇怪的事情时谨慎的优化器可以比忽略奇怪现象的优化器更安全地在不奇怪的地方更加积极地进行优化。 - supercat
如果您对标准中的规则有问题,应该撰写一篇WG21论文,最好具有比“奇怪”更精确的别名允许规范。在SO答案上发布冗长的评论说明当前规则并不能改变它们。 - T.C.

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