重新诠释联盟与不同的工会

Reinterpreting a union to a different union

本文关键字:新诠释 联盟      更新时间:2023-10-16

我有一个标准的layout联合,其中有很多类型:

union Big {
    Hdr h;
    A a;
    B b;
    C c;
    D d;
    E e;
    F f;
};

A thru F中的每种类型都是标准layout,它是其第一个成员Hdr类型的对象。Hdr标识了联盟的活跃成员,因此这是类似变体的。现在,我正在确定(因为我检查过(活动成员是BC。有效地,我将空间缩小到:

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的目标是有效地用作文档,我已经检查了它是BC,因此将来没有人添加代码来检查它是A或其他东西。

我将B子对象读成B的事实是否足以使这种形成良好?可以在此处有意义地使用常见的初始序列规则吗?

能够将指针置于a,并将其重新解释为b的指针,必须是 pointer-interconvertible

指针交换性是关于对象,而不是对象的类型

在C 中,某个地方有对象。如果您在特定位置有一个Big,至少有一个成员存在,则由于指针互连而在同一位置也有一个Hdr

但是,该位置没有Little对象。如果那里没有Little对象,则不能使用不存在的Little对象的指针交换。

假设它们是平面数据(普通的旧数据,可复制等(。

这意味着您可以复制他们的字节表示,并且可以使用。实际上,优化器似乎了解到一个堆栈本地缓冲区的纪念品,一个新的位置(带有琐碎的构造函数(,然后一个emcpy后退实际上是一个不p的。

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

因此,如果我是对的,并且当BigLittle中类型的子类型时,BigLittle与布局兼容,则可以这样做:

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不执行任何内存分配;它使用新的位置,该位置在data点使用data处的CC_28点的位置构建T。虽然它看起来像在做很多事情(复制内存(,但它优化为NOOP。


c 具有"对象存在"的概念。对象的存在几乎与物理或抽象机器中写入的碎屑或字节几乎无关。您的二进制中没有指示与"现在存在的对象"相对应。

但是语言有这个概念。

不存在的对象无法与之相互作用。如果这样做,C 标准将无法定义程序的行为。

这允许优化器对您的代码所做的工作,不做什么,以及无法达到哪些分支的假设以及可以达到哪些分支。它让编译器做出不明的假设;通过指针或引用A 无法通过指针或引用B进行的数据修改数据,除非A和B以某种方式存在于同一地点。

编译器可以证明BigLittle对象都不能同时存在。因此,没有通过指针或对Little引用的任何数据修改任何数据可以修改Big类型变量中存在的任何内容。反之亦然。

想象一下given_b_or_c是否修改了字段。好吧,编译器可以在given_biggiven_b_or_cuse_a_b上进行内联,请注意,没有修改Big的实例(只是Little的一个实例(,并证明Big的数据字段在调用您的代码之前被拨打了您的代码之前,无法修改。

p>这节省了负载指令,优化器非常高兴。但是现在您有了读取的代码:
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,因此由于异叠性规则。

现在这样做:

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并消除它。

laundry_pod的实时示例被优化。请注意,如果未进行优化,则代码必须具有条件和printf。但是因为是,它已被优化到空程序中。

我在N4296(C 14标准(中找不到措辞,这将使这一合法。更重要的是,我无法找到任何给定的措辞:

union Big2 {
    Hdr h;
    A a;
    B b;
    C c;
    D d;
    E e;
    F f;
};

我们可以将Big引用到Big2的引用中,然后使用参考。(请注意, BigBig2 bitut-compatible 。(

这是省略的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对象,并且Ergo没有这样的成员。由于标准"省略了这种情况的任何明确定义",因此该行为是不确定的。

我不确定,如果这确实适用。在reinterpret_cast-注释部分中,他们谈论了指针交换对象。

和[basic.compound]/4:

两个对象 a b pointer-interconvertible if:

  • 它们是同一对象,或
  • 一个是联合对象,另一个是该对象的非静态数据成员,或者
  • 一个是标准的类对象,另一个是该对象的第一个非静态数据成员,或者,如果对象没有非静态数据成员,则该对象的第一个基类子对象或
  • 存在一个对象 c ,因此 a c 是指针交换,以及 c b 是指针交换。

如果两个对象是指针交换的,则它们具有相同的地址,并且可以通过reinterpret_­cast从一个指针获得一个指针。

在这种情况下,我们将Hdr h;(c(作为两个工会中的非静态数据成员

Big* (a) -> Hdr* (c) -> Little* (b)