C和C++中联合的目的

leg*_*s2k 237 c c++ unions type-punning

我早先使用过工会; 今天,当我读到这篇文章并开始知道这段代码时,我感到震惊

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
Run Code Online (Sandbox Code Playgroud)

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

更新:

事后我想澄清一些事情.

  • 这个问题的答案与C和C++不一样; 我无知的年轻自我将其标记为C和C++.
  • 在仔细研究了C++ 11的标准后,我无法确切地说它调用了访问/检查非活动的union成员是未定义/未指定/实现定义的.我能找到的只是§9.5/ 1:

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

  • 在C中,(C99 TC3 - DR 283以后)这样做是合法的(感谢Pascal Cuoq提出这个问题).但是,如果读取的值对于读取的类型无效(所谓的"陷阱表示"),尝试执行此操作仍会导致未定义的行为.否则,读取的值是实现定义的.
  • C89/90在未指明的行为(附件J)中称之为,而K&R的书称其实施已定义.来自K&R的报价:

    这是联合的目的 - 一个可以合法地保存几种类型中的任何一种的变量.[...]只要用法一致:检索到的类型必须是最近存储的类型.程序员有责任跟踪当前存储在联合中的类型; 如果将某些内容存储为一种类型并将其提取为另一种类型,则结果将依赖于实现.

  • 从Stroustrup的TC++ PL中提取(强调我的)

    使用工会对于数据的兼容性至关重要[...] 有时会被误用于"类型转换 ".

最重要的是,这个问题(自我提出以来其标题保持不变)的目的是为了理解工会的目的而不是标准允许的内容例如,当然,C++标准允许使用继承进行代码重用.将继承作为C++语言特性引入并不是目的或初衷.这就是安德烈的答案继续作为公认的答案的原因.

AnT*_*AnT 371

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

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

它就像一个酒店的房间.不同的人生活在不重叠的时期.这些人永远不会见面,而且通常对彼此一无所知.通过妥善管理房间的分时(即确保不同的人不同时分配到一个房间),一个相对较小的酒店可以为相对较多的人提供住宿,这就是酒店是给.

这正是联盟所做的.如果您知道程序中的多个对象包含具有非重叠值生命周期的值,那么您可以将这些对象"合并"到一个联合中,从而节省内存.就像酒店房间在每个时刻最多只有一个"活跃"租户一样,工会在每个节目时刻最多只有一个"活跃"成员.只能读取"活动"成员.通过写入其他成员,您可以将"活动"状态切换到该其他成员.

由于某种原因,联盟的这个原始目的得到了"覆盖"与完全不同的东西:写一个联盟的一个成员,然后通过另一个成员检查它.这种记忆重新解释(又名"打字")不是对工会的有效使用.它通常导致未定义的行为被描述为在C89/90中产生实现定义的行为.

编辑:使用工会进行打字(即写一个成员然后再读另一个成员)在C99标准的技术勘误表中给出了更详细的定义(参见DR#257DR#283).但请记住,正确地说,这并不能通过尝试读取陷阱表示来防止您遇到未定义的行为.

  • +1精心制作,给出一个简单的实际例子,并谈论工会的遗产! (35认同)
  • @AndreyT"直到最近才使用工会进行类型惩罚从来都不合法":2004年不是"非常近期",特别是考虑到只有C99最初是笨拙的措辞,似乎通过工会未定义类型惩罚.实际上,虽然工会的类型惩罚在C89中是合法的,在C11中是合法的,并且它在C99中一直是合法的,尽管委员会在2004年之前修正了不正确的措辞以及随后的TC3发布.http://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_283.htm (31认同)
  • 我对这个答案的问题是,我见过的大多数操作系统都有头文件,可以做到这一点.例如,我在Windows和Unix上的旧(64位)版本的`<time.h>`中都看到了它.如果我要被要求理解以这种方式工作的代码,那么将其视为"无效"和"未定义"并不足够. (6认同)
  • @ legends2k编程语言由标准定义.C99标准的技术勘误3在其脚注82中明确允许打字,我邀请您自己阅读.这不是电视采访摇滚明星并表达他们对气候变化的看法.Stroustrup的观点对C标准所说的没有任何影响. (6认同)
  • @ legends2k"_我知道任何个人的意见都无关紧要,只有标准吗?"编译器编写者的意见比(极差)语言"规范"更重要. (6认同)
  • 打字怎么样? (2认同)
  • @AndreyT:那么现在进行这种双关语并且仍然是可移植的是否合法(如 C 和 C++ 中的标准化)?如果是这样,哪些版本的语言标准使它神圣化? (2认同)
  • @bobobobo:您所看到的不一定相关,并且在任何情况下都不是“不同意”的理由。直到最近对语言规范进行更正之前,该语言明确禁止使用联合来进行“别名”(即类型双关)。尽管现代 C 语言允许这种用法,但这仍然不是联合体的最初目的。正如我的回答中所述,引入联合是为了内存共享,而不是为了类型双关。 (2认同)
  • 赞成该帖子中的第一句话.当然,你可以详细说明为什么人们使用工会进行打字; 我相信你知道这不只是出于"某种"原因.:) (2认同)
  • @TED只是因为它未定义,这并不意味着它是不可预测的; 它只是意味着语言规范没有说明你尝试时会发生什么.这种情况可以被有效地视为"非正式定义",因为大多数编译器供应商专门编写其编译器代码以允许联合用于类型惩罚(因为如果它不起作用它会破坏大量代码).但是,依赖于此行为最终会在遇到没有编译器的情况下导致问题,或者如果任何编译器确实改变了它们处理联合的方式. (2认同)
  • @AnT 引用“明确禁止”的相关部分?我在[此处](http://flash-gordon.me.uk/ansi.c.txt)的一些早期草案中发现的是这样的文本(3.3.2.3):“除了一个例外,如果联合对象的成员在将值存储在对象的不同成员中之后访问,该行为是实现定义的。/33/”。 (2认同)

Eri*_*ler 36

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

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

    union
    {
        int intValue;
        double dblValue;
        char *strValue;
    } value;
} object;
Run Code Online (Sandbox Code Playgroud)

  • 传说:在某些情况下,你根本无法做到这一点.在Java中使用Object时,在C中使用类似VAROBJECT的东西. (3认同)
  • 我完全同意,在不进入未定义行为混乱的情况下,也许这是我能想到的工会最好的预期行为;但当我只是使用“int”或“char*”来表示 10 个 object[] 项时,不会浪费空间;在这种情况下,我实际上可以为每种数据类型声明单独的结构而不是 VAROBJECT?难道它不会减少混乱并使用更少的空间吗? (2认同)

Dav*_*eas 34

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

如果你正在使用C++(你使用的是两个标签)并且你真的关心可移植性,那么你可以使用结构并提供一个setter,它uint32_t通过位掩码操作来获取和设置字段.使用函数可以在C中完成相同的操作.

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

  • @legends2k,问题是优化器可能会假设 uint32_t 不会通过写入 uint8_t 进行修改,因此当优化使用该假设时,您会得到错误的值...@Joe,一旦您访问 uint32_t ,就会出现未定义的行为指针(我知道,有一些例外)。 (2认同)

bob*_*obo 18

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

考虑以下:

union Vector3f
{
  struct{ float x,y,z ; } ;
  float elts[3];
}
Run Code Online (Sandbox Code Playgroud)

这是做什么的?它允许Vector3f vec;通过任何名称干净,整洁地访问成员:

vec.x=vec.y=vec.z=1.f ;
Run Code Online (Sandbox Code Playgroud)

或者通过整数访问数组

for( int i = 0 ; i < 3 ; i++ )
  vec.elts[i]=1.f;
Run Code Online (Sandbox Code Playgroud)

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

  • 这不是打字.在我的例子中,类型_match_,所以没有"双关语",它只是别名. (4认同)
  • 这也被称为`type-punning`,这也在问题中提到.此外,问题中的示例显示了一个类似的示例. (3认同)
  • 是的,但是从语言标准的绝对角度来看,写入和读取的成员是不同的,这在问题中是不确定的。 (3认同)
  • 我希望将来的标准可以解决此特殊情况,使其在“常见的初始子序列”规则下被允许。但是,在当前措辞下,数组不参与该规则。 (3认同)
  • @curiousguy:显然没有要求在没有任意填充的情况下放置结构成员.如果代码测试结构成员放置或结构大小,如果访问直接通过联合进行,代码应该有效,但严格读取标准将表明获取联合或结构成员的地址会产生一个无法使用的指针作为其自己类型的指针,但必须首先转换回指向封闭类型或字符类型的指针.任何可远程操作的编译器都会通过使更多的东西工作而扩展语言... (3认同)

小智 9

正如你所说,这是严格未定义的行为,尽管它将在许多平台上"起作用".使用联合的真正原因是创建变体记录.

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;
Run Code Online (Sandbox Code Playgroud)

当然,您还需要某种鉴别器来说明变体实际包含的内容.请注意,在C++中,联合使用并不多,因为它们只能包含POD类型 - 实际上是那些没有构造函数和析构函数的类型.


Tot*_*nga 7

在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;
Run Code Online (Sandbox Code Playgroud)

在litlle内存时,这个结构使用的内存少于拥有所有成员的结构.

顺便提一下C

    typedef struct {
      unsigned int mantissa_low:32;      //mantissa
      unsigned int mantissa_high:20;
      unsigned int exponent:11;         //exponent
      unsigned int sign:1;
    } realVal;
Run Code Online (Sandbox Code Playgroud)

访问位值.


Pau*_*l R 5

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

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

Mat*_* M. 5

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

它的性能与enum + union构造相同(堆栈分配太多等),但它使用类型的模板列表而不是enum:)


Nic*_*ick 5

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

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