【问题标题】:Casting a Struct to an Array [duplicate]将结构转换为数组[重复]
【发布时间】:2015-05-30 10:32:28
【问题描述】:

这是一个严格的别名问题,因为编译器会导致任何优化顺序问题。

假设我在struct XMFLOAT3 中有三个公共floats(与this one 不同。)我想转换为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声明的实现。

【问题讨论】:

  • 结构体可能包含填充不是问题吗?从编译器开始放入实际的填充将是一个迂回的举动,但优化器可能会假设这样的填充不能被正确的代码检测到。此外,(&amp;foo.x)[2] 看起来像是一个普通的越界数组访问,这对编译器来说是显而易见的。
  • @MSalters,是的,可能会有填充,尽管在理论上和实践中填充只会出于对齐目的而添加,并且三个相邻的浮动成员将与 a 的三个元素对齐float[3],所以它确实是狡猾的。 (&amp;foo.x)[2] 等价于 *(&amp;foo.x + 2) 并且只要该地址确实有一个 float,3.9.2/3 就会使其格式正确,这又回到了填充和对齐。
  • @MSalters 似乎Jonathan Wakely 的答案中的static_assert 可用于防止笨拙的编译器。 (无意冒犯啮齿动物。)
  • @JonathanWakely:“这可能是为了对齐”不是一个详尽的列表。如果意图是只允许用于对齐的填充,则该语句将大致是“不应有初始填充。除了满足直接在此类填充之后的成员的对齐要求外,其他任何地方都不应有填充”。
  • @JonathanMee 从第一个成员事物中添加了该索引的欺骗。

标签: c++ arrays struct alias reinterpret-cast


【解决方案1】:

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

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

【讨论】:

  • 我认为更具体的问题是:“XMFLOAT3*float* 兼容吗?”
【解决方案2】:

reinterpret_castXMFLOAT3*float* 是可以的,因为:

9.2 [class.mem] 第 20 段:

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

这意味着第一个成员的地址是结构的地址,当您访问*bar 时不涉及别名,因为您通过float 类型的左值访问float,这很好.

但演员表也不需要,相当于第一个版本:

auto bar = &foo.x;

表达式bar[2]只有在结构成员之间没有填充时才可以,或者更准确地说,如果数据成员的布局与数组float[3]相同,在这种情况下为3.9.2 [basic.compound] 第 3 段说没关系:

对象指针类型的有效值表示内存中字节的地址 (1.7) 或空指针 (4.10)。如果 T 类型的对象位于地址 A,则 cv T* 类型的指针其值为 据说地址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 的出色建议。我很感激你的提醒。
  • 在 Meta 上,OP expressed concern 认为这个答案的知名度将低于类似问题的答案,尽管(在他看来)这个问题的重复性远远优于重复的答案。考虑到这一点,您可能希望对其中一个或两个受骗者发布类似的答案。
【解决方案3】:

考虑一个相当聪明的编译器:

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 这就是问题所在,但我相信编译器不允许在cout 之后重新排序bar[2] += 5.0f;,因为我有引用在我的问题中。
  • 不,我描述的是超出范围的数组访问。严格的别名是union { int x; float z } foo(&amp;foo.x)[0]foo.z 重叠。这不再是越界访问,但现在变成了严格的别名违规。为了发生严格的别名,您首先需要一个 valid 表达式来引用保存另一个不兼容类型的对象的内存。 bar[2] 根本无效。
  • @JonathanMee:至于“重新排序”,我假设编译器优化了对bar[2] 的整个赋值,因为没有更多代码取决于barx 的值.
  • 为什么 3.9.2 [basic.compound] 第 3 段不适用于bar[2]?该注释似乎非常相关: "[ 注意:例如,数组末尾的地址(5.7)将被视为指向可能位于该地址的数组元素类型的不相关对象。 ...]" 你是否建议一个元素越界但不是两个?
  • @JonathanWakely:允许在对象之后直接形成地址。但是您可能无法读取或写入该地址,这仍然是越界访问。 +2 马上就出来了。
猜你喜欢
  • 1970-01-01
  • 2015-06-22
  • 2011-10-04
  • 1970-01-01
  • 2013-05-15
  • 2018-02-26
  • 2023-04-08
  • 2014-11-13
  • 2018-04-19
相关资源
最近更新 更多