将结构体强制转换为数组

3
这是一个关于严格别名问题的疑问,即编译器是否会因此导致任何优化顺序问题。
比如说我有三个公共的floatstruct XMFLOAT3中(类似于这个)。我想强制转换成一个float*。这会导致优化问题吗?
XMFLOAT3 foo = {1.0f, 2.0f, 3.0f};
auto bar = &foo.x;

bar[2] += 5.0f;
foo.z += 5.0f;
cout << foo.z;

我假设这段代码总是会输出"13"。但是对于下面的代码呢:
XMFLOAT3 foo = {1.0f, 2.0f, 3.0f};
auto bar = reinterpret_cast<float*>(&foo);

bar[2] += 5.0f;
foo.z += 5.0f;
cout << foo.z;

我认为这是合法的,因为根据http://en.cppreference.com/w/cpp/language/reinterpret_cast#Type_aliasing
T2是聚合类型或联合类型,其中包含一个前述类型作为元素或非静态成员(包括递归地包含子聚合的元素和包含它的联合的非静态数据成员):这使得从结构体的第一个成员和联合的元素向包含它的结构体/联合转换是安全的。
我的理解正确吗?
显然,这将取决于XMFLOAT3的声明,具体实现可能会有所不同。

1
问题不是结构体可能包含填充吗?编译器放入实际填充是一种狡猾的举动,但优化器可能会假设这种填充对正确代码是不可检测的。此外,(&foo.x)[2]看起来像是一个明显的越界数组访问,编译器很容易就能发现。 - MSalters
1
@MSalters,是的,可能会有填充,尽管在理论和实践中,填充只会为了对齐而添加,三个相邻的浮点数成员与float[3]的三个元素一样对齐,所以这确实是一个棘手的问题。(&foo.x)[2]等同于*(&foo.x + 2),3.9.2/3规定只要该地址确实有一个float,那么它就是良好形式,这又回到了填充和对齐的问题。 - Jonathan Wakely
1
@MSalters,看起来Jonathan Wakely的答案中的static_assert可以用来防止一些不靠谱的编译器。(无意冒犯啊,小动物们) - Jonathan Mee
1
@JonathanWakely:“这可能是为了对齐而完成的”并不是详尽无遗的列表。如果意图仅允许填充以进行对齐,则该语句将大致为“不得有初始填充。除了满足直接跟随此类填充的成员的对齐要求之外,任何其他地方都不得填充”。 - MSalters
1
@JonathanMee 给第一个成员的东西添加了一个重复索引。 - Baum mit Augen
显示剩余9条评论
3个回答

5
XMFLOAT3*float*reinterpret_cast是可以的,因为:

根据9.2 [class.mem]第20段:

如果标准布局类对象有任何非静态数据成员,则其地址与其第一个非静态数据成员的地址相同。 否则,其地址与其第一个基类子对象(如果有)的地址相同。[注意:因此,在标准布局结构对象中可能存在未命名填充,但不在其开头,以实现适当的对齐。 —end note]

这意味着第一个成员的地址是结构体的地址,当您通过类型为float的lvalue访问*bar时,不存在别名问题,因为您正在访问float,这没问题。

但是转换也是不必要的,它等同于第一个版本:

auto bar = &foo.x;

表达式bar[2]只有在结构体成员之间没有填充或更精确地说,数据成员的布局与数组float [3]相同,此时3.9.2 [basic.compound]第3段指出该情况是可以接受的:

对象指针类型的有效值表示内存中字节的地址(1.7)或空指针(4.10)。如果一个类型为T的对象位于一个地址A上,则类型为cvT*的指针,其值为地址A,被称为指向该对象的指针,无论该值是如何获得的。

实际上,同一类型的三个相邻非静态数据成员的布局方式很可能与数组相同(我认为Itanium ABI也保证了这一点),但为了安全起见,您可以添加以下内容:
 static_assert(sizeof(XMFLOAT3)==sizeof(float[3]),
     "XMFLOAT3 layout must be compatible with float[3]");

或者是出于偏执,或者只是在z后面添加了额外的成员:
 static_assert(offsetof(XMFLOAT3, y)==sizeof(float)
               && offsetof(XMFLOAT3, z)==sizeof(float)*2,
     "XMFLOAT3 layout must be compatible with float[3]");

显然,这将取决于XMFLOAT3的声明实现。

是的,它依赖于它是一个标准布局类类型,并且依赖于其数据成员的顺序和类型。


关于 static_assert 的优秀建议,非常感谢你的提醒。 - Jonathan Mee
在 Meta 上,OP 表达了担忧,认为这个答案会比被关闭为重复的类似问题上的答案获得更少的关注,尽管在他看来,这个答案要比重复问题上的答案优秀得多。考虑到这一点,您可能希望在其中一个或两个重复问题上发布类似的答案。 - Mark Amery

0

这是完全有效的;这与严格别名无关。

严格别名规则要求彼此别名的指针具有兼容的类型;
显然,float*float* 兼容。


我认为更具体的问题是:“XMFLOAT3* 是否与 float* 兼容?” - Jonathan Mee

-1

考虑一个相当智能的编译器:

XMFLOAT3 foo = {1.0f, 2.0f, 3.0f}; 
auto bar = &foo.x;

bar[2] += 5.0f;
foo.z += 5.0f; // Since no previous expression referenced .z, I know .z==8.0
cout << foo.z; // So optimize this to a hardcoded cout << 8.0f

将变量访问和操作替换为已知结果是一种常见的优化方法。在这里,优化器看到了三个使用 .z 的地方:初始赋值、增量和最终使用。它可以轻松确定这三个点的值,并进行替换。

由于结构体成员不能重叠(不像联合体),从 .x 派生出来的 bar 不能与 .z 重叠,因此 .bar[2] 不能影响 .z

正如您所看到的,一个完全正常的优化器可能会产生“错误”的结果。


你所描述的是Strict Aliasing。这就是问题所在,但我相信编译器不允许在我的问题中引用之后重新排序bar[2] += 5.0f;cout - Jonathan Mee
不,我所描述的是数组越界访问。严格别名是指当union {int x; float z} foo允许(&foo.x)[0]foo.z重叠时。这不再是越界访问,而是严格别名违规。要出现严格别名,首先需要一个有效的表达式来引用持有另一种不兼容类型对象的内存。bar[2]根本不合法。 - MSalters
为什么3.9.2 [basic.compound]第3段不适用于bar[2]?注释似乎非常相关:“[注:例如,数组结束后的地址(5.7)将被认为指向该地址可能位于该地址的数组元素类型的不相关对象。...]"您是否建议可以超出一个元素的边界,但不能超出两个元素的边界? - Jonathan Wakely
@JonathanWakely:在对象之后直接形成地址是允许的。但是你不能从那个地址读取或写入数据,否则仍然会造成越界访问。+2 就完全不行了。 - MSalters
不,3.9.2并没有提到读取或写入。它说&foo.x+2可以指向该位置上的另一个相同类型的对象。因此,如果没有填充(可以通过静态断言证明),并且foo.z在该地址上,则*(&foo.x+2)访问它,因此bar[2]是可以的。3.9.2没有讨论形成无法解引用的地址,它说指针指向一个对象,无论如何获得该值。 - Jonathan Wakely
显示剩余4条评论

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