将联合重新解释为不同的联合

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.

所以,如果我是正确的,并BigLittle是布局兼容时,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,Tdata使用字节的点处构造a data.虽然看起来它正在做很多事情(复制内存),但它会优化为noop.


有一个"对象存在"的概念.对象的存在几乎与在物理或抽象机器中写入的位或字节无关.您的二进制文件上没有与"现在存在对象"相对应的指令.

但语言有这个概念.

不存在的对象无法与之交互.如果这样做,C++标准不会定义程序的行为.

这允许优化器假设您的代码执行什么,不执行什么操作以及无法访问哪些分支以及哪些分支可以到达.它允许编译器进行无混叠假设; 通过指针或对A的引用修改数据不能改变通过指针或对B的引用到达的数据,除非A和B都存在于同一点.

编译器可以证明Big并且Little对象不能同时存在于同一位置.因此,不能通过指针或引用修改任何数据,以Little修改类型变量中存在的任何内容Big.反之亦然.

想象一下,如果given_b_or_c修改一个字段.那么编译器会内联given_biggiven_b_or_cuse_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.但因为它是,它被优化为空程序.


Mar*_*ica 4

我在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,然后使用该引用。(请注意BigBig2布局兼容的。)

  • 在 [basic.lval]/10.6 中据说可以使用“Big2”或“Little”类型的引用来访问“Big”类型的对象,请参阅我的答案。 (2认同)