C和C ++中的合并目的


254

我以前很舒服地使用过工会。今天,当我阅读这篇文章并得知此代码时,我感到震惊

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的标准进行搜索之后,我不能得出结论说它要求访问/检查非活动工会成员是未定义/未指定/实现定义的。我只能找到§9.5/ 1:

    如果一个标准布局联合包含多个共享公共初始序列的标准布局结构,并且此标准布局联合类型的对象包含一个标准布局结构,则可以检查任何一个的标准初始结构标准布局的struct成员。§9.2/ 19:如果相应成员具有与布局兼容的类型,并且两个成员都不是一个位字段,或者两个成员都是宽度相同的一个或多个初始序列的位字段,则两个标准布局结构共享一个公共的初始序列成员。

  • 在C语言中(从C99 TC3-DR 283开始),这样做是合法的(感谢Pascal Cuoq提出了这一点)。但是,如果读取的值恰好对于读取的类型无效(所谓的“陷阱表示”),则尝试执行此操作仍可能导致未定义的行为。否则,读取的值是实现定义的。
  • C89 / 90在未指定的行为下对此进行了声明(附件J),K&R的书说它是定义的实现。来自K&R的报价:

    这是联合的目的-单个变量可以合法地容纳多种类型之一。[...]只要用法是一致的:检索的类型必须是最近存储的类型。程序员有责任跟踪工会中当前存储的是哪种类型。如果将某种内容存储为一种类型并提取为另一种类型,则结果取决于实现。

  • 摘自Stroustrup的TC ++ PL(重点是我的)

    联合使用对于有时被误用于“类型转换 ” 的数据兼容性至关重要。

最重要的是,提出这个问题(自我提出要求以来,其标题保持不变)的目的是理解联合的目的,而不是标准允许的条件。例如,C ++标准当然允许使用继承进行代码重用,但是将继承引入C ++语言特性并不是目的或初衷。这就是Andrey的答案继续保持被接受的原因。


11
简而言之,允许编译器在结构中的元素之间插入填充。因此,b, g, r,并且a可能不是连续的,因此与的布局不匹配uint32_t。这是其他人指出的Endianess问题的补充。
Thomas Matthews'2

8
这就是为什么您不应该标记问题C和C ++的原因。答案是不同的,但是由于应答者甚至不知道他们在回答什么标签(他们甚至知道吗?),所以您会感到垃圾。
Pascal Cuoq

5
@downvoter感谢您的解释,我了解您希望我神奇地理解您的抱怨,以后不再重复:P
legends2k 2014年

1
关于拥有工会的初衷,请记住,C标准将C工会的日期推迟了几年。快速浏览Unix V7会显示一些通过联合进行的类型转换。
ninjalj 2015年

3
scouring C++11's standard I couldn't conclusively say that it calls out accessing/inspecting a non-active union member is undefined [...] All I could find was §9.5/1...真?您在段落开始处引用了一个异常注释,而不是要点“在联合中,最多一个非静态数据成员可以在任何时间处于活动状态,即,最多一个非静态数据成员可以处于活动状态。非静态数据成员可以随时存储在联合中。” -直到p4:“通常,必须使用显式的析构函数调用和放置新的运算符来更改联合的活动成员
underscore_d

Answers:


407

工会的目的很明显,但是由于某些原因人们经常错过工会。

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

就像旅馆的房间一样。不同的人在其中生活了不重叠的时间。这些人从不见面,而且彼此之间一无所知。通过适当地管理房间的分时(例如,确保不同的人不会同时分配到一个房间),相对较小的酒店可以为相对大量的人提供住宿,这就是酒店是给。

这正是工会所做的。如果您知道程序中的几个对象保存的值具有不重叠的值寿命,则可以将这些对象“合并”为一个并集,从而节省内存。就像旅馆房间在每个时间最多只有一个“活动”租户一样,工会在每个计划时间都最多有一个“活跃”成员。只能读取“活动”成员。通过写入其他成员,可以将“活动”状态切换到该其他成员。

由于某种原因,工会的最初目的被完全不同的东西“压倒了”:写工会的一个成员,然后通过另一个成员检查它。这种内存重新解释(也称为“类型校正”)不是对联合的有效使用。它通常导致未定义的行为在C89 / 90中被描述为产生实现定义的行为。

编辑:使用联合进行类型修剪的目的(即写一个成员,然后读另一个成员)在C99标准的技术勘误之一中给出了更详细的定义(请参阅DR#257DR#283)。但是,请记住,这在形式上并不能防止您尝试读取陷阱表示形式而陷入未定义的行为。


37
+1是精心制作的,它给出了一个简单的实际示例,并讲述了工会的遗产!
legends2k 2010年

6
我对这个答案的问题是,我见过的大多数操作系统都具有执行此操作的头文件。例如,我已经在<time.h>Windows和Unix的旧(64位)版本中看到了它。如果要我去理解以这种确切方式工作的代码,将其视为“无效”和“未定义”是远远不够的。
TED

31
@AndreyT“直到最近才使用联合体进行类型校正从未合法”:2004并不是“最近”,特别是考虑到最初只是措辞拙劣的C99,似乎使通过联合体的类型转换不明确。实际上,尽管联合工会在C89中是合法的,在C11中是合法的,在C99中一直是合法的,尽管委员会直到2004年才修复了错误的措辞,并随后发布了TC3。open-std.org/jtc1/sc22/wg14/www/docs/dr_283.htm
Pascal Cuoq

6
@ legends2k编程语言是由标准定义的。C99标准的技术勘误3明确允许在其脚注82中进行类型校正,我邀请您自己阅读。这不是在电视中接受摇滚明星采访并表达他们对气候变化的看法的电视。Stroustrup的意见对C标准所说的内容无效。
Pascal Cuoq

6
@ legends2k“ 我知道任何人的意见都没有关系,只有标准才重要 ”编译器作者的意见比(极差的)语言“规范”重要得多。
curiousguy 2015年

38

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

struct VAROBJECT
{
    enum o_t { Int, Double, String } objectType;

    union
    {
        int intValue;
        double dblValue;
        char *strValue;
    } value;
} object;

我完全同意,在不进入不确定行为混乱的情况下,这也许是我能想到的工会的最佳预期行为;但是当仅使用,说intchar*用于10项object [] 时,不会浪费空间。在那种情况下,我实际上可以为每种数据类型声明单独的结构,而不是VAROBJECT?它不会减少混乱并使用更少的空间吗?
legends2k 2010年

3
图例:在某些情况下,您根本无法做到这一点。在Java中使用Object的情况相同,在C中使用VAROBJECT之类的东西。
Erich Kitzmueller

正如您所解释的,带标签的联合的数据结构似乎是联合的唯一合法使用。
legends2k 2014年

还给出一个有关如何使用值的示例。
西罗Santilli郝海东冠状病六四事件法轮功

1
@CiroSantilli新疆改造中心六四事件法轮功来自C ++ Primer的示例的一部分可能会有所帮助。wandbox.org/permlink/cFSrXyG02vOSdBk2
Rick

34

从语言的角度来看,行为是不确定的。考虑到不同的平台在内存对齐和字节序方面可能有不同的约束。大字节序与小字节序的机器中的代码将以不同的方式更新结构中的值。要修复该语言中的行为,将要求所有实现都使用相同的字节序(和内存对齐约束...)来限制使用。

如果您使用的是C ++(使用的是两个标签),并且您真的在乎可移植性,那么您可以使用struct并提供一个setter,该setter可以uint32_t通过bitmask操作来获取和设置字段。使用C函数可以完成相同的操作。

编辑:我希望AProgrammer写下一个投票答案并关闭该答案。正如一些评论所指出的那样,在标准的其他部分中处理字节顺序是通过让每个实现决定要执行的操作,并且对齐方式和填充方式也可以以不同的方式处理。现在,AProgrammer隐式引用的严格别名规则在这里很重要。允许编译器对变量的修改(或不修改)进行假设。在合并的情况下,编译器可以对指令重新排序,并将每个颜色分量的读取移到对color变量的写入上。


+1即可获得简单明了的回复!我同意,对于可移植性,您在第二段中给出的方法很有效;但是,如果我的代码绑定到单一体系结构(付出代价的代价),我可以使用我提出的问题的方式,因为它为每个像素值节省了4个字节,并且节省了运行该函数的时间?
legends2k 2010年

字节序问题不会强制标准将其声明为未定义的行为-reinterpret_cast具有与字节序问题完全相同的问题,但是实现定义了行为。
JoeG 2010年

1
@ legends2k,问题是优化器可能会假设未通过写入uint8_t来修改uint32_t,因此,当优化使用该假设时,您会得到错误的值... @Joe,访问指针(我知道,有一些例外)。
AProgrammer

1
@ legends2k / AProgrammer:reinterpret_cast的结果是实现定义的。使用返回的指针不会导致未定义的行为,只会导致实现定义的行为。换句话说,该行为必须是一致的和定义的,但是它不是可移植的。
JoeG

1
@ legends2k:任何体面的优化器都会识别选择整个字节的按位操作,并生成用于读取/写入字节的代码,与并集相同,但定义明确(并且可移植)。例如uint8_t getRed()const {return color&0x000000FF; } void setRed(uint8_t r){color =(colour&〜0x000000FF)| r; }
Ben Voigt 2010年

22

我经常遇到的最常见用法union别名

考虑以下:

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

这是做什么的?它允许使用Vector3f vec;以下任一名称对的成员进行干净整洁的访问:

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

或通过整数访问数组

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

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


3
这个问题也被type-punning提到。问题中的示例也显示了类似的示例。
legends2k 2013年

4
它不是类型修饰。在我的示例中,类型match,所以没有“ pun”,它只是别名。
bobobobo

3
是的,但是从语言标准的绝对角度来看,写入和读取的成员是不同的,这在问题中是不确定的。
legends2k

3
我希望将来的标准可以解决此特殊情况,使其在“常见的初始子序列”规则下得到允许。但是,在当前措辞下,数组不参与该规则。
Ben Voigt 2014年

3
@curiousguy:显然没有要求将结构成员放置在没有任意填充的地方。如果代码对结构成员的放置或结构大小进行测试,则如果直接通过并集进行访问,则代码应该可以工作,但是严格阅读该标准将表明获取并集或结构成员的地址会产生无法使用的指针作为其自身类型的指针,但必须首先将其转换回指向封闭类型或字符类型的指针。任何可远程工作的编译器都将通过使更多的工作超出...来扩展语言
。– supercat

10

就像您说的那样,尽管它在许多平台上都可以“工作”,但这绝对是不确定的行为。使用联合的真正原因是创建变体记录。

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 ++中,并集并没有太多用处,因为它们只能包含POD类型-实际上是没有构造函数和析构函数的类型。


您是否以此方式使用过(就像在问题中一样?)?:)
legends2k 2010年

这有点花哨,但我不太接受“各种记录”。就是说,我确定他们是牢记在心的,但是如果优先考虑,为什么不提供它们呢?“提供构建基块是因为它也可能对构建其他事物很有用”,看起来似乎更直观。尤其是考虑到至少一个或多个应用程序,它可能是在心中-内存映射I / O寄存器,其中输入和输出寄存器(而重叠)与自己的名字不同的实体,类型等
Steve314

@ Stev314如果这是他们考虑的用途,则可以使它成为未定义的行为。

@Neil:首先说出+1的实际使用情况,但不影响未定义的行为。我想他们可以像其他类型的修剪操作(reinterpret_cast等)一样定义实现。但是,就像我问的那样,您是否将其用于类型调整?
legends2k 2010年

@Neil-内存映射的寄存器示例不是未定义的,通常的endian / etc除外,并给出了“ volatile”标志。在此模型中写入地址不会像读取相同地址那样引用相同的寄存器。因此,不存在“不读回什么内容”的问题,因为您不回读-写入该地址的任何输出,当您读取时,您只是在读取一个独立的输入。唯一的问题是确保您阅读并集的输入端并写入输出端。在嵌入式产品中很常见-可能仍然如此。
2010年

8

在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;

在具有少量内存的情况下,此结构所使用的内存要少于具有所有成员的结构。

顺便说一下C提供

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

访问位值。


尽管您的两个示例均在标准中进行了完美定义;但是,嘿,使用位字段可以确定是无法移植的代码,不是吗?
legends2k 2010年

不,不是。据我所知,它得到了广泛的支持。
Totonga'2

1
编译器支持不会转换为可移植的。C书C(因此C ++)不保证机器字中字段的顺序,因此,如果出于后一个原因使用它们,则程序不仅将是不可移植的,而且还将依赖于编译器。
legends2k

5

尽管这是严格未定义的行为,但实际上,它将与几乎所有编译器一起使用。这种范例被广泛使用,以至于任何自重的编译器在这种情况下都需要做“正确的事”。它肯定比类型处理优先,后者可能会在某些编译器中生成残破的代码。


2
是否没有字节序问题?与“未定义”相比,这是一个相对容易的修复程序,但如果有的话,值得考虑一些项目。
2010年

5

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

它的性能与enum + union构造相同(也分配了堆栈等),但是它使用类型的模板列表代替enum:)


5

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

而且,工会不仅为了节省空间。它们可以帮助现代编译器进行类型校正。如果您拥有reinterpret_cast<>所有内容,编译器将无法对您的工作做出假设。它可能必须舍弃对类型的了解,然后重新开始(强制写回内存,与CPU时钟速度相比,这几天的效率很低)。


4

从技术上讲,它是未定义的,但实际上,大多数(全部?)编译器都将其与使用reinterpret_cast从一种类型到另一种类型的方式完全相同,其结果是实现定义的。我不会因为您当前的代码而睡不着。


从一种类型到另一种类型的reinterpret_cast,其结果由实现定义。 ”不,不是。实现不必定义它,大多数都不需要定义它。另外,将某个随机值强制转换为指针的允许实现定义的行为是什么?
curiousguy 2011年

4

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


4

其他人则提到了架构差异(小到大字节序)。

我读到这样一个问题,因为变量的内存是共享的,所以通过写一个变量,其他变量会更改,并且根据变量的类型,该值可能毫无意义。

例如。union {float f; 我 } X;

如果您随后从xf中读取内容,则对xi的写入将毫无意义-除非您打算这样做以查看浮点数的符号,指数或尾数成分。

我认为还有一个对齐问题:如果某些变量必须按单词对齐,那么您可能无法获得预期的结果。

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

假设,在某些机器上,必须将char进行字对齐,那么c [0]和c [1]将与i共享存储,但不共享c [2]和c [3]。


一个必须字对齐的字节?这是没有意义的。根据定义,字节没有对齐要求。
curiousguy 2011年

是的,我可能应该使用一个更好的例子。谢谢。
philcolbourn

@curiousguy:在许多情况下,可能希望字节数组按字对齐。如果一个数组有很多数组,例如1024字节,并且经常希望将它们复制到另一个数组,那么在许多系统上使它们字对齐可能会使a的速度翻倍memcpy()。由于某些原因,某些系统可能会推测性地对齐发生在结构/联合外部的char[]分配。在现有示例中,将使的所有元素重叠的假设是不可移植的,但这是因为无法保证。ic[]sizeof(int)==4
2015年

4

在1974年记录的C语言中,所有结构成员共享一个公共名称空间,“ ptr-> member”的含义定义为将成员的位移添加到“ ptr”并使用成员的类型访问结果地址。这种设计使得可以使用相同的ptr,其成员名称取自不同的结构定义,但具有相同的偏移量。程序员将该功能用于多种目的。

当为结构成员分配了自己的名称空间时,就不可能声明两个具有相同位移的结构成员。在语言中添加联合使实现与该语言早期版本中可用的语义相同的语义成为可能(尽管仍无法使用查找/替换来替换名称foo-> member来将名称导出到封闭上下文中)进入foo-> type1.member)。重要的不是要让加入联合的人员记住任何特定的目标用法,而是要提供一种方法,无论出于何种目的,依靠早期语义的程序员仍然应该能够实现目标。即使他们必须使用不同的语法来实现相同的语义。


欣赏历史课,但是用未定义的标准进行定义,在过去的C时代(K&R书籍是唯一的“标准”)不是这种情况,必须确保不要将其用于任何目的和进入UB土地。
legends2k

2
@ legends2k:编写标准时,大多数C实现都以相同的方式对待并集,并且这种处理很有用。但是,只有少数几个没有,并且该标准的作者不愿意将任何现有的实现都标记为“不符合”。相反,他们认为,如果实施者不需要标准来告诉他们做某事(事实已经证明他们已经在做),则未指定或未定义该标准只会保留现状。认为它应该使事情比标准制定之前就没有那么明确了……
supercat

2
...似乎是一项更新得多的创新。所有这一切特别令人难过的是,如果针对高端应用程序的编译器作者想出办法,如何向1990年代大多数编译器实现的语言中添加有用的优化指令,而不是破坏那些仅受“仅“90%实现的,其结果将是其可以比超现代C.表现得更好和更可靠地一种语言
supercat

2

可以使用并集有两个主要原因:

  1. 一种方便的方法,以不同的方式访问相同的数据,例如您的示例
  2. 当有不同的数据成员(其中只有一个可以处于活动状态)时,一种节省空间的方法

1实际上,这是一种C风格的技巧,可以在您了解目标系统的内存体系结构工作原理的基础上简化代码编写。如前所述,如果您实际上并不针对许多不同的平台,那么通常可以摆脱它。我相信某些编译器还可以让您使用打包指令(我知道它们在结构上也可以)?

在COM中广泛使用的VARIANT类型中可以找到一个很好的例子2 .。


2

正如其他人提到的那样,可以将结合枚举并包装到结构中的并集用于实现带标签的并集。一种实际用途是实现Rust Result<T, E>,它最初是使用pure 来实现的enum(Rust可以在枚举变量中保存其他数据)。这是一个C ++示例:

template <typename T, typename E> struct Result {
    public:
    enum class Success : uint8_t { Ok, Err };
    Result(T val) {
        m_success = Success::Ok;
        m_value.ok = val;
    }
    Result(E val) {
        m_success = Success::Err;
        m_value.err = val;
    }
    inline bool operator==(const Result& other) {
        return other.m_success == this->m_success;
    }
    inline bool operator!=(const Result& other) {
        return other.m_success != this->m_success;
    }
    inline T expect(const char* errorMsg) {
        if (m_success == Success::Err) throw errorMsg;
        else return m_value.ok;
    }
    inline bool is_ok() {
        return m_success == Success::Ok;
    }
    inline bool is_err() {
        return m_success == Success::Err;
    }
    inline const T* ok() {
        if (is_ok()) return m_value.ok;
        else return nullptr;
    }
    inline const T* err() {
        if (is_err()) return m_value.err;
        else return nullptr;
    }

    // Other methods from https://doc.rust-lang.org/std/result/enum.Result.html

    private:
    Success m_success;
    union _val_t { T ok; E err; } m_value;
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.