Bar*_*rry 35 c++ language-lawyer c++11
我有一个标准布局联合,其中包含一大堆类型:
union Big {
Hdr h;
A a;
B b;
C c;
D d;
E e;
F f;
};
Run Code Online (Sandbox Code Playgroud)
每种类型的A直通F是标准布局和具有作为其第一元件类型的对象Hdr.该Hdr标识了工会的积极成员为,所以这是变体样.现在,我处于一种我确定知道的情况(因为我检查过),活跃成员是a B或a C.实际上,我已将空间减少到:
union Little {
Hdr h;
B b;
C c;
};
Run Code Online (Sandbox Code Playgroud)
现在,是以下明确定义还是未定义的行为?
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);
}
}
Run Code Online (Sandbox Code Playgroud)
的目标Little是有效地作为文档,我已经检查,这是一个B或者C等将来无人添加代码,以检查它是一个A什么的.
事实上,我正在阅读这个B子对象,B足以使这个格式良好吗?这里可以有意义地使用公共初始序列规则吗?
Yak*_*ont 18
为了能够获取指向A的指针,并将其重新解释为指向B的指针,它们必须是指针可互换的.
指针可互换是关于对象,而不是对象的类型.
在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;
}
Run Code Online (Sandbox Code Playgroud)
上面的函数在运行时是一个noop(在优化的构建中),但它将T-layout兼容的数据data转换为实际的T.
所以,如果我是正确的,并Big和Little是布局兼容时,Big在类型的子类型Little,你可以这样做:
Little* inplace_to_little( Big* big ) {
return laundry_pod<Little>(big);
}
Big* inplace_to_big( Little* big ) {
return laundry_pod<Big>(big);
}
Run Code Online (Sandbox Code Playgroud)
要么
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 ...
}
}
Run Code Online (Sandbox Code Playgroud)
如果Big有非平坦的数据(如引用或const数据),上面的内容可怕.
注意,laundry_pod不做任何内存分配; 它使用placement new,T在data使用字节的点处构造a 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),并证明了从数据字段Big之前调用你的代码不能被修改了它的缓存.
这节省了一个加载指令,优化器非常高兴.但现在您的代码如下:
Big b = whatever;
b.foo = 7;
((Little&)b).foo = 4;
if (b.foo!=4) exit(-1);
Run Code Online (Sandbox Code Playgroud)
这是优化的
Big b = whatever;
b.foo = 7;
((Little&)b).foo = 4;
exit(-1);
Run Code Online (Sandbox Code Playgroud)
因为它可以证明它b.foo必须7被设置一次并且永远不会被修改.访问权限Little无法修改Big由于别名规则.
现在这样做:
Big b = whatever;
b.foo = 7;
(*laundry_pod<Little>(&b)).foo = 4;
Big& b2 = *laundry_pod<Big>(&b);
if (b2.foo!=4) exit(-1);
Run Code Online (Sandbox Code Playgroud)
并假设大的那里没有变化,因为有一个memcpy和一个::new可以合法地改变数据状态.没有严格的别名违规.
它仍然可以遵循memcpy并消除它.
活生生的例子中laundry_pod被优化掉.请注意,如果没有优化,代码必须具有条件和printf.但因为它是,它被优化为空程序.
我在n4296 (C++14 标准草案)中找不到任何可以使其合法的措辞。更重要的是,我什至找不到任何给出的措辞:
union Big2 {
Hdr h;
A a;
B b;
C c;
D d;
E e;
F f;
};
Run Code Online (Sandbox Code Playgroud)
我们可以将reinterpret_cast一个引用转化Big为一个引用Big2,然后使用该引用。(请注意Big和Big2是布局兼容的。)