C和C ++中联合的目的

时间:2010-02-22 11:17:20

标签: c++ c unions type-punning

我早先使用过工会;今天,当我阅读this post并开始知道这段代码

时,我感到震惊
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

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

更新

事后我想澄清一些事情。

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

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

  • 在C中,(C99 TC3 - DR 283起)这样做是合法的(thanks to Pascal Cuoq来实现这一点)。但是,如果读取的值对于读取它的类型无效(所谓的“陷阱表示”),尝试执行它仍然可能导致未定义的行为。否则,读取的值是实现定义。
  • C89 / 90在未指明的行为(附件J)中称之为,而K& R的书称其实施已定义。引自K& R:

      

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

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

      

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

最重要的是,这个问题(自我的要求以来其标题保持不变)的目的是为了理解工会的目的而不是标准允许的问题例如。当然,使用继承进行代码重用是C ++标准允许的,但是it wasn't the purpose or the original intention of introducing inheritance as a C++ language feature。这就是安德烈的答案继续作为公认的答案的原因。

16 个答案:

答案 0 :(得分:345)

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

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

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

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

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

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

答案 1 :(得分:34)

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

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

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

答案 2 :(得分:34)

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

如果您正在使用C ++(您使用的是两个标签)并且您真的关心可移植性,那么您可以使用该结构并提供一个带有uint32_t的setter并通过位掩码操作适当地设置字段。使用函数可以在C中完成相同的操作。

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

答案 3 :(得分:17)

我经常遇到的union {em>常用使用别名

请考虑以下事项:

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;

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

答案 4 :(得分: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;

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

答案 5 :(得分: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;

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

顺便提一下

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

访问位值。

答案 6 :(得分:5)

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

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

答案 7 :(得分:4)

虽然这是严格的未定义行为,但实际上它几乎适用于任何编译器。它是一种广泛使用的范例,任何自尊的编译器都需要在这种情况下做“正确的事情”。它肯定比类型惩罚更受欢迎,它可能会在一些编译器中产生破碎的代码。

答案 8 :(得分:4)

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

答案 9 :(得分:4)

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

我读到的问题是,由于变量的内存是共享的,然后通过写入一个,其他的更改,并且根据其类型,值可能毫无意义。

例如。     联盟{       漂浮f;       int i;     } x;

如果你从x.f读取,写入x.i将毫无意义 - 除非你想要查看浮点数的符号,指数或尾数组成部分。

我认为还存在对齐问题:如果某些变量必须是字对齐的,那么您可能无法获得预期的结果。

例如。     联盟{       char c [4];       int i;     } x;

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

答案 10 :(得分:4)

对于实际使用联合的另一个示例,CORBA框架使用标记联合方法序列化对象。所有用户定义的类都是一个(巨大的)联合的成员,而integer identifier告诉demarshaller如何解释联合。

答案 11 :(得分:4)

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

此外,工会不是只是来节省空间。他们可以帮助现代编译器打字。如果你reinterpret_cast<>,编译器无法对你正在做的事情做出假设。它可能不得不丢弃它对你的类型的了解并重新开始(强制写回内存,这与CPU时钟速度相比,效率非常低)。

答案 12 :(得分:3)

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

当为结构成员分配了自己的命名空间时,它变得不可能 声明具有相同位移的两个结构成员。添加联盟 该语言使得实现与之相同的语义成为可能 在早期版本的语言中可用(尽管无法使用 导出到封闭上下文的名称可能仍然需要使用 find / replace将foo-&gt;成员替换为foo-&gt; type1.member)。什么是 重要的不是那些加入工会的人有什么特别的 记住目标用法,而是它们提供程序员的手段 仍然依赖于早期语义的用于任何目的 能够实现相同的语义,即使他们必须使用不同的语义 这样做的语法。

答案 13 :(得分:2)

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

  1. 以不同方式访问相同数据的便捷方式,例如您的示例
  2. 当存在不同的数据成员时,可以节省空间的方法,其中只有一个可以“活跃”
  3. 1在您了解目标系统的内存架构如何工作的基础上,确实更像是一种C风格的黑客攻击。如前所述,如果你实际上没有针对很多不同的平台,你通常可以侥幸逃脱。我相信一些编译器也可能让你使用包装指令(我知道它们在结构上有用)?

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

答案 14 :(得分:0)

正如其他人所提到的,联合与枚举结合并包装到结构中可用于实现带标签的联合。一种实际用途是实现Rust的Result<T, E>,它最初是使用纯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;
}

答案 15 :(得分:0)

@bobobobo代码正确无误,如@Joshua所指出的(不幸的是,我不允许添加注释,因此请在此处执行,IMO错误的决定是一开始就不允许这样做):

https://en.cppreference.com/w/cpp/language/data_members#Standard_layout告诉您这样做很好,至少从C ++ 14起

在具有非工会类类型T1的活动成员的标准版式联合中,允许读取另一个非工会类类型T2的联合成员的非静态数据成员m,前提是m是T的一部分。 T1和T2的常见初始序列(除了未定义通过非易失性glvalue读取易失性成员外)。

因为在当前情况下,T1和T2仍然捐赠相同的类型。