自 C++11 起,
std::complex<T>[n]
保证可别名为 T[n*2]
,并具有明确定义的值。这正是人们对任何主流架构所期望的。
对于我自己的类型,这种保证可以通过标准 C++ 实现吗?
struct vec3 { float x, y, z; }
还是只有在编译器的特殊支持下才可能实现?
TL;DR:编译器必须检查
reinterpret_cast
并找出涉及 std::complex
的(标准库)特化。我们无法一致地模仿语义。
我认为很明显,将三个不同的成员视为数组元素是行不通的,因为指向它们的指针的指针算术受到极大的限制(例如,加 1 会产生一个超过末尾的指针)。
因此,我们假设
vec3
包含三个 int
的数组。
即使这样,您隐式需要的底层 reinterpret_cast<int*>(&v)
(其中 v
是 vec3
)也不会为您留下指向第一个元素的指针。请参阅指针可互换性的详尽要求:
两个对象
和a
是 指针可相互转换的,如果:b
它们是同一个对象,或者
一个是标准布局联合对象,另一个是该对象的非静态数据成员([class.union]),或者
一个是标准布局类对象,另一个是该对象的第一个非静态数据成员,或者,如果该对象没有 非静态数据成员,该对象的第一个基类子对象 ([class.mem]),或
存在一个对象
,使得c
和a
是指针可相互转换的,并且c
和c
是 指针可相互转换。b
如果两个对象是指针可相互转换的,那么它们具有相同的 地址,并且可以从指针获得指向一的指针 通过
到另一个。 [ 注意:数组对象 和它的第一个元素不是指针可相互转换的,即使 他们有相同的地址。 — 尾注 ]reinterpret_cast
这是相当明确的;虽然我们可以获得指向数组的指针(作为第一个成员),并且虽然指针互换性是传递的,但我们无法获得指向其第一个元素的指针。
最后,即使你设法获得了指向成员数组第一个元素的指针,如果你有一个
vec3
数组,你也无法使用简单的指针增量来遍历所有成员数组,因为我们得到了指针超过中间数组的末尾。 launder 也不能解决这个问题,因为与指针关联的对象不共享任何存储(具体信息参见 [ptr.launder])。
这只有在编译器的特殊支持下才有可能实现。
联合不会让你到达那里,因为常见的方法实际上具有未定义的行为,尽管布局兼容的初始序列有例外,并且你可以通过
unsigned char*
作为特殊情况检查对象。不过就是这样。
有趣的是,除非我们假设“下面”具有广泛且无用的含义,否则该标准在这方面在技术上是矛盾的:
[..] 下面列出了可以使用reinterpret_cast 显式执行的转换。使用reinterpret_cast 无法显式执行其他转换。[C++14: 5.2.10/1]:
complex<T>
的情况就没有被提及。最后,您所指的规则是在很久以后的[C++14: 26.4/4]
中引入的。
我认为如果您的类型包含
vec3
,并且您确保 float x[3]
,那么它适用于单个 sizeof(vec3) == 3*sizeof(float) && is_standard_layout_v<vec3>
。考虑到这些条件,标准保证第一个成员的偏移量为零,因此第一个 float
的地址是对象的地址,并且您可以执行数组算术来获取数组中的其他元素:
struct vec3 { float x[3]; } v = { };
float* x = reinterpret_cast<float*>(&v); // points to first float
assert(x == v.x);
assert(&x[0] == &v.x[0]);
assert(&x[1] == &v.x[1]);
assert(&x[2] == &v.x[2]);
您不能将
vec3
数组视为三倍长度的浮点数数组。对每个 vec3
内的数组进行数组算术不允许您访问下一个 vec3
内的数组。 CWG 2182 与此处相关。