我有一个标准布局联合,其中包含大量类型:
union Big {
Hdr h;
A a;
B b;
C c;
D d;
E e;
F f;
};
每个类型A
到F
都是标准布局,并且第一个成员是Hdr
类型的对象。 Hdr
标识了union的活动成员是什么,所以这是变体的。现在,我确实知道(因为我检查过)活跃成员是B
还是C
。实际上,我已将空间缩小到:
union Little {
Hdr h;
B b;
C c;
};
现在,以下定义明确或未定义的行为是什么?
void given_big(Big const& big) {
switch(big.h.type) {
case B::type: // fallthrough
case C::type:
given_b_or_c(reinterpret_cast<Little const&>(big));
break;
// ... other cases here ...
}
}
void given_b_or_c(Little const& little) {
if (little.h.type == B::type) {
use_a_b(little.b);
} else {
use_a_c(little.c);
}
}
Little
的目标是有效地充当文档,我已经检查过它是B
或C
所以将来没有人添加代码检查它是A
还是其他什么。
我是否正在将B
子对象作为B
阅读,以使其格式良好?可以在这里有意义地使用公共初始序列规则吗?
答案 0 :(得分:18)
为了能够获取指向A的指针,并将其重新解释为指向B的指针,它们必须是指针可互换的。
Pointer-interconvertible是关于对象,而不是对象类型。
在C ++中,地方有对象。如果您在特定地点有一个Big
并且至少有一个成员存在,那么由于指针可互换性,同一地点也会有一个Hdr
。
但是那个地方没有Little
个对象。如果那里没有Little
个对象,则它不能与指针可互换,而Little
对象不存在。
它们似乎是布局兼容的,假设它们是平面数据(普通旧数据,可以轻易复制等)。
这意味着你可以复制他们的字节表示并且它可以工作。事实上,优化器似乎理解堆栈本地缓冲区的memcpy,一个新的放置(使用普通的构造函数),然后memcpy实际上是一个noop。
template<class T>
T* laundry_pod( void* data ) {
static_assert( std::is_pod<Data>{}, "POD only" ); // could be relaxed a bit
char buff[sizeof(T)];
std::memcpy( buff, data, sizeof(T) );
T* r = ::new( data ) T;
std::memcpy( data, buff, sizeof(T) );
return r;
}
上述函数在运行时是一个noop(在优化版本中),但它将data
的T-layout兼容数据转换为实际T
。
因此,如果我是对的,Big
和Little
在Big
是Little
中的类型的子类型时是布局兼容的,您可以这样做:< / p>
Little* inplace_to_little( Big* big ) {
return laundry_pod<Little>(big);
}
Big* inplace_to_big( Little* big ) {
return laundry_pod<Big>(big);
}
或
void given_big(Big& big) { // cannot be const
switch(big.h.type) {
case B::type: // fallthrough
case C::type:
auto* little = inplace_to_little(&big); // replace Big object with Little inplace
given_b_or_c(*little);
inplace_to_big(little); // revive Big object. Old references are valid, barring const data or inheritance
break;
// ... other cases here ...
}
}
如果Big
包含非平面数据(如引用或const
数据),则上述情况可能非常糟糕。
请注意laundry_pod
不进行任何内存分配;它使用展示位置new,使用T
处的字节在data
指向的位置构建data
。虽然看起来它正在做很多事情(复制内存),但它会优化为noop。
c++有一个“对象存在”的概念。对象的存在几乎与在物理或抽象机器中写入的位或字节无关。您的二进制文件上没有与“现在存在对象”相对应的指令。
但语言有这个概念。
不存在的对象无法与之交互。如果这样做,C ++标准不会定义程序的行为。
这允许优化器假设您的代码执行什么,不执行什么操作以及无法访问哪些分支以及哪些分支可以到达。它允许编译器进行无混叠假设;通过指针或对A 的引用修改数据不能更改通过指针或B引用到达的数据,除非A和B都存在于同一位置。
编译器可以证明Big
和Little
对象不能同时存在于同一位置。因此,不能通过指针或对Little
的引用来修改任何数据,而是可以修改Big
类型的变量中存在的任何内容。反之亦然。
想象一下,如果given_b_or_c
修改字段。那么编译器可以内联given_big
和given_b_or_c
以及use_a_b
,注意没有Big
的实例被修改(只是Little
的一个实例),并证明1}}在调用代码之前缓存的数据字段无法修改。
这为它保存了一条加载指令,优化器非常高兴。但现在您的代码如下:
Big
被优化为
Big b = whatever;
b.foo = 7;
((Little&)b).foo = 4;
if (b.foo!=4) exit(-1);
因为它可以证明Big b = whatever;
b.foo = 7;
((Little&)b).foo = 4;
exit(-1);
必须是b.foo
它已设置一次且从未修改过。由于别名规则,7
访问无法修改Little
。
现在这样做:
Big
并假设大的那里没有变化,因为有一个memcpy和Big b = whatever;
b.foo = 7;
(*laundry_pod<Little>(&b)).foo = 4;
Big& b2 = *laundry_pod<Big>(&b);
if (b2.foo!=4) exit(-1);
可以合法地改变数据的状态。没有严格的别名冲突。
它仍然可以跟随::new
并消除它。
memcpy
的{{3}}被优化了。请注意,如果没有优化,代码必须具有条件和printf。但因为它是,它被优化为空程序。
答案 1 :(得分:4)
我在n4296(草案C ++ 14标准)中找不到任何可以使其合法化的措辞。更重要的是,我不能甚至找到任何给出的措辞:
union Big2 {
Hdr h;
A a;
B b;
C c;
D d;
E e;
F f;
};
我们可以reinterpret_cast
引用Big
引用Big2
,然后使用引用。 (请注意,Big
和Big2
布局兼容。)
答案 2 :(得分:2)
这是UB遗漏。 [expr.ref] /4.2:
如果E2是非静态数据成员且E1的类型是“
cq1 vq1 X
”, 并且E2的类型是“cq2 vq2 T
”,表达式[E1.E2
]表示 第一个表达式指定的对象的命名成员。
在评估given_b_or_c
中的given_big
来电期间,little.h
中的对象表达式实际上并未指定Little
个对象,并且那里没有这样的成员。因为标准&#34;省略了任何明确的行为定义&#34;对于这种情况,行为是未定义的。
答案 3 :(得分:-1)
我不确定,这是否真的适用于此。在reinterpret_cast
- Notes部分,他们讨论了指针可互换的对象。
如果出现以下情况, a 和 b 两个对象指针可互换:
- 它们是同一个对象,或
- 一个是union对象,另一个是该对象的非静态数据成员,或
- 一个是标准布局类对象,另一个是该对象的第一个非静态数据成员,或者,如果该对象没有非静态数据成员,则该对象的第一个基类子对象,或者< / LI>
- 存在一个对象 c ,使 a 和 c 是指针可互换的, c 和< em> b 是指针可互换的。
如果两个对象是指针可互换的,那么它们具有相同的地址,并且可以通过
reinterpret_cast
从指向另一个的指针获得一个指针。
在这种情况下,我们将Hdr h;
(c)作为两个联合中的非静态数据成员,这应该允许(因为第二个和最后一个项目符号点)
Big* (a) -> Hdr* (c) -> Little* (b)