C和C ++中工会的目的

早些时候我已经习惯使用工会了, 今天我看到这篇文章,并且知道这个代码,我感到震惊

union ARGB { uint32_t colour; struct componentsTag { uint8_t b; uint8_t g; uint8_t r; uint8_t a; } components; } pixel; pixel.colour = 0xff040201; // ARGB::colour is the active member from now on // somewhere down the line, without any edit to pixel if(pixel.components.a) // accessing the non-active member ARGB::components 

实际上是未定义的行为,即从最近写的一个工会成员读取,导致未定义的行为。 如果这不是工会的预期用法,那是什么? 有人可以详细解释吗?

更新:

我想事后澄清几件事情。

  • 对于C和C ++来说,这个问题的答案是不一样的。 我的无知年轻的自我标记为C和C ++。
  • 通过C ++ 11的标准,我无法确定地说它调用访问/检查非活动联合成员是未定义的/未指定的/实现定义的。 我能find的是§9.5/ 1:

    如果标准布局联合包含多个共享初始序列的标准布局结构,并且如果此标准布局联合types的对象包含其中一个标准布局结构,则允许检查任何公共初始序列标准布局结构成员。 §9.2/ 19:如果相应的成员具有布局兼容的types,并且这两个标准布局结构共享一个共同的初始序列,并且这两个成员都不是一个位域,或者都是一个或多个初始序列的宽度相同的位域成员。

  • 而在C,( C99 TC3 – DR 283起),这样做是合法的( 感谢Pascal Cuoq提出这一点)。 但是,尝试这样做的操作仍然会导致未定义的行为 ,如果读取的值发生错误(所谓的“陷阱表示”),则读取它的types。 否则,读取的值是实现定义的。
  • C89 / 90在非特定的行为下(附件J)对此进行了说明,而K&R的书则说明了它的实现定义。 来自K&R的报价:

    这是一个联合的目的 – 一个单一的variables,可以合法地持有几种types的任何一种。 只要用法一致:检索的types必须是最近存储的types。 程序员有责任跟踪当前存储在工会中的types; 如果某个东西被存储为一个types并且被提取为另一个types,则结果是实现相关的。

  • 从Stroustrup的TC ++ PL中提取(重点是我的)

    对于“types转换有时误用的数据的兼容性,使用联合可能是必不可less的。

最重要的是,这个问题(自从我的问题以来,其标题保持不变)的目的是理解工会的目的,而不是标准允许的东西。例如,使用inheritance代码重用当然是C ++标准允许的,但将inheritance作为C ++语言function的目的或原意并不是这样 。 这就是安德烈的答案继续保持被接受的原因。

工会的目的是相当明显的,但由于某种原因,人们经常错过它。

联合的目的是通过使用相同的内存区域在不同的时间存储不同的对象来节省内存 而已。

这就像一个酒店的房间。 不同的人生活在非重叠的时期。 这些人从来不见面,一般都不了解对方。 通过妥善pipe理房间分时(即确保不同的人同时分配到一个房间),相对较小的酒店可以为相对较多的人提供住宿,这是什么酒店是给。

这正是联盟所做的。 如果您知道程序中有多个对象保存的值不存在重叠的值,那么您可以将这些对象“合并”为一个联合,从而节省内存。 就像酒店房间在每个时刻最多有一个“主动”租户一样,一个工会在每个时间点最多只有一个“主动”成员。 只有“主动”成员可以阅读。 通过写入其他成员,您将“活动”状态切换到其他成员。

出于某种原因,工会的这个最初目的被完全不同的东西“覆盖”:写一个工会的成员,然后通过另一个成员检查。 这种内存重新解释(又名“types双关”) 不是工会的有效使用。 它通常导致未定义的行为被描述为在C89 / 90中产生实现定义的行为。

编辑:使用工会为types捻(即写一个成员,然后阅读另一个)的目的被给予更详细的定义在C99标准的技术勘误之一(见DR#257和DR#283 )。 但是,请记住,正式的这不会通过尝试读取陷阱表示来防止陷入未定义的行为。

从语言的angular度来看,行为是不确定的。 考虑到不同的平台在内存alignment和字节顺序方面可能有不同的限制。 大端序与小端序机器中的代码将不同地更新结构中的值。 修复语言中的行为将需要所有的实现使用相同的字节顺序(和内存alignment约束…)限制使用。

如果您正在使用C ++(您正在使用两个标记),并且您真的关心可移植性,那么您可以使用该结构并提供一个setter,它接受uint32_t并通过位掩码操作适当地设置字段。 用C函数也可以做到这一点。

编辑 :我期待AProgrammer写下投票的答案,并closures这一个。 正如一些评论所指出的,通过让每个实现决定要做什么,sorting和填充也可以以不同的方式处理,从而在标准的其他部分处理sorting。 现在,AProgrammer暗指的严格的别名规则在这里是一个重要的点。 编译器允许对variables的修改(或缺less修改)做出假设。 在联合的情况下,编译器可以对指令进行重新sorting,并将每个颜色分量的读取移到写入颜色variables上。

您可以使用联合来创build如下所示的结构,其中包含一个字段,用于告诉我们联合的哪个组件是实际使用的:

 struct VAROBJECT { enum o_t { Int, Double, String } objectType; union { int intValue; double dblValue; char *strValue; } value; } object; 

我经常碰到的union常见的用法是别名

考虑以下几点:

 union Vector3f { struct{ float x,y,z ; } ; float elts[3]; } 

这是做什么的? 它允许一个Vector3f vec;干净,整洁的访问Vector3f vec; 任何一个名字的成员:

 vec.x=vec.y=vec.z=1.f ; 

或者通过整数访问数组

 for( int i = 0 ; i < 3 ; i++ ) vec.elts[i]=1.f; 

在某些情况下,按名称访问是您可以做的最清晰的事情。 在其他情况下,尤其是当以编程方式select轴时,更容易做的是通过数字索引访问轴 – 0代表x,1代表y,2代表z。

正如你所说,这是严格未定义的行为,虽然它将在许多平台上“工作”。 使用工会的真正原因是创build变体logging。

 union A { int i; double d; }; A a[10]; // records in "a" can be either ints or doubles a[0].i = 42; a[1].d = 1.23; 

当然,你也需要一些辨别器来说出变体实际包含的内容。 注意,在C ++中,联合体并没有多大用处,因为它们只能包含PODtypes – 实际上那些没有构造函数和析构函数的types。

在C中,这是实现类似变体的一种很好的方式。

 enum possibleTypes{ eInt, eDouble, eChar } struct Value{ union Value { int iVal_; double dval; char cVal; } value_; possibleTypes discriminator_; } switch(val.discriminator_) { case eInt: val.value_.iVal_; break; 

在litlle内存的时候,这个结构比拥有所有成员的结构使用更less的内存。

顺便说一下C提供的

  typedef struct { unsigned int mantissa_low:32; //mantissa unsigned int mantissa_high:20; unsigned int exponent:11; //exponent unsigned int sign:1; } realVal; 

访问位值。

虽然这是严格未定义的行为,但实际上它将与几乎任何编译器一起工作。 这是一个广泛使用的范例,任何自尊的编译器都需要在这种情况下做“正确的事情”。 当然这比string更好,这可能会产生一些编译器的破解代码。

从技术上讲,这是不确定的,但实际上大多数(所有)编译器将它与使用从一种types到另一种types的reinterpret_cast完全相同,其结果是实现定义的。 我不会失去你当前的代码睡眠。

在C ++中, Boost Variant实现了union的安全版本,旨在尽可能地防止未定义的行为。

它的性能是相同的enum + union结构(堆栈分配太等),但它使用types的模板列表,而不是enum 🙂

有关实际使用联合的更多示例,CORBA框架使用标记联合方法序列化对象。 所有用户定义的类都是一个(巨大的)联合的成员, 整数标识符告诉demarshaller如何解释联合。

行为可能是不确定的,但这只是意味着没有“标准”。 所有体面的编译器都提供#pragmas来控制打包和alignment,但可能有不同的默认值。 默认值也将根据所使用的优化设置而改变。

此外,工会不仅仅是为了节省空间。 他们可以帮助现代编译器打字。 如果你reinterpret_cast<>那么编译器就不能对你正在做什么做出假设。 它可能不得不扔掉它所知道的关于你的types的东西,然后重新开始(迫使写回到内存,与CPU时钟速度相比这是非常低效的)。

其他人提到了架构差异(小 – 大端)。

我读到的问题是,因为variables的内存是共享的,那么通过写入一个variables,其他variables就会改变,根据它们的types,这个值可能是没有意义的。

例如。 union {float f; int i; } X;

如果你从xf读取数据,写入xi将是毫无意义的 – 除非你想要查看浮点数的符号,指数或尾数部分。

我认为还有一个alignment的问题:如果一些variables必须字alignment,那么你可能不会得到预期的结果。

例如。 union {char c [4]; int i; } X;

如果假设某个机器上的字符必须是字alignment的,那么c [0]和c [1]将与i共享存储空间,而不是c [2]和c [3]。

在1974年所logging的C语言中,所有结构成员都共享一个通用名称空间,“ptr-> member”的含义被定义为将成员的位移添加到“ptr”并使用成员的types访问得到的地址。 这种devise使得可以使用相同的ptr,从不同的结构定义中获取成员名称,但是具有相同的偏移量; 程序员将这种能力用于各种目的。

当结构成员被赋予它们自己的命名空间时,就不可能声明两个具有相同位移的结构成员。 在语言中添加联合可以实现语言的早期版本中可用的相同语义(尽pipe无法将名称导出到封闭上下文中,仍然可能需要使用查找/replace来replacefoo-> member进入foo-> type1.member)。 重要的不是join工会的人有任何特定的目标用法,而是提供了一种方法,依靠先前语义的程序员无论出于何种目的都应该能够实现相同的语义,即使他们必须使用不同的语法来做到这一点。

你可以使用联盟有两个主要的原因:

  1. 一种方便的方式,以不同的方式访问相同的数据,如你的例子
  2. 当存在不同的数据成员时,一种节省空间的方法,其中只有一个数据成员可以“活跃”

1在您了解目标系统的内存架构如何工作的基础上,实际上更多的是C编写的快捷编写代码。 正如已经说过的,如果你实际上不是针对很多不同的平台的话,你通常可以逃避。 我相信一些编译器也可以让你使用包装指令(我知道他们在结构上做)?

2.在COM中广泛使用的VARIANTtypes中可以find一个很好的例子。