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的标准之后,我不能肯定地说它调用访问/检查一个非活动的工会成员是未定义的/未指定的/由实现定义的。我只能找到§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++语言特性的目的或初衷. C。这就是为什么安德烈的回答仍然被人们所接受的原因。

146979 次浏览

正如你所说,这是严格未定义的行为,尽管它将“工作”在许多平台上。使用联合的真正原因是为了创建不同的记录。

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类型——实际上是那些没有构造函数和析构函数的类型。

尽管这是严格未定义的行为,但实际上它适用于几乎任何编译器。它是一种被广泛使用的范例,任何有自尊的编译器都需要在这种情况下做“正确的事情”。它当然比类型双关语更受欢迎,在某些编译器中,类型双关语很可能会生成坏代码。

你可以使用联合来创建像下面这样的结构体,它包含一个字段,告诉我们联合的哪个组件实际被使用:

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


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

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

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

编辑:我期待一个程序员写下一个答案来投票并关闭这个。正如一些评论所指出的,在标准的其他部分,通过让每个实现决定做什么来处理字节性,对齐和填充也可以以不同的方式处理。现在,AProgrammer隐式引用的严格混叠规则是这里的重点。编译器可以对变量的修改(或不修改)做出假设。在联合的情况下,编译器可以重新排序指令,并将每个颜色组件的读操作移到颜色变量的写操作之上。

你可以使用 a一个联合,主要有两个原因:

  1. 一种以不同方式访问相同数据的方便方法,就像在您的示例中一样
  2. 当存在不同的数据成员,其中只有一个可以“活动”时,一种节省空间的方法

1实际上更像是一个c风格的黑客,在你知道目标系统的内存架构是如何工作的基础上,以捷径编写代码。就像之前所说的,如果你没有瞄准许多不同的平台,你便能够避开这一问题。我相信一些编译器可能也会让你使用打包指令(我知道他们在结构上这样做)?

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

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

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

访问位值。

在c++中,提高变体实现了联合的安全版本,旨在尽可能地防止未定义的行为。

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

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

我读到的问题是,由于变量的内存是共享的,那么写入一个变量,其他变量就会改变,根据它们的类型,值可能是没有意义的。

< p >。 联盟{ 浮动f; int我; } x; < / p >

如果你从x.f读取数据,那么写入x.i是没有意义的——除非你想要查看浮点数的符号、指数或尾数分量。

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

< p >。 联盟{ 字符c [4]; int我; } x; < / p >

假设,在某些机器上,一个char必须字对齐,那么c[0]和c[1]将与i共享存储空间,而不是c[2]和c[3]。

再举一个联合实际使用的例子,CORBA框架使用带标签的联合方法序列化对象。所有用户定义的类都是一个(巨大的)联合的成员,整数标识符告诉解编码器如何解释该联合。

结合的目的是相当明显的,但由于某种原因,人们经常忽略它。

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

它就像旅馆里的一个房间。不同的人住在里面的时间不重叠。这些人从来没有见过面,而且通常对彼此一无所知。通过合理管理房间的分时(即确保不同的人不会同时被分配到一个房间),一个相对较小的酒店可以为相对大量的人提供住宿,这就是酒店的目的。

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

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

编辑:为了类型双关的目的使用联合(即写入一个成员然后读取另一个成员)在C99标准的一个技术更正中给出了更详细的定义(参见# 257博士# 283博士)。但是,请记住,在形式上,这并不能防止您在试图读取陷阱表示时遇到未定义的行为。

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

此外,为了节省空间,联合也不是只是。它们可以帮助现代编译器使用类型双关语。如果你reinterpret_cast<>所有东西,编译器就不能假设你在做什么。它可能不得不放弃它所知道的类型并重新开始(强制写回内存,与CPU时钟速度相比,这是非常低效的)。

我经常遇到的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。

在1974年记录的C语言中,所有结构成员共享一个公共名称空间,“ptr->成员”的含义是定义 成员的位移到“ptr”,并使用 成员的类型。这种设计使得成员可以使用相同的ptr 名称取自不同的结构定义,但偏移量相同; 程序员将这种能力用于各种目的 当结构成员被分配自己的名称空间时,这就不可能了 声明具有相同位移的两个结构成员。加入工会 这种语言使得实现与过去相同的语义成为可能 在该语言的早期版本中可用(尽管无法拥有 导出到封闭上下文的名称可能仍然需要使用 Find /replace将foo->成员替换为foo->type1.member)。是什么 重要的不是加入工会的人有什么特别之处 考虑到目标的使用,而是它们提供了一种方法,使程序员 谁依赖于早期的语义无论出于什么目的,应该仍然 能够实现相同的语义,即使他们不得不使用不同的

正如其他人提到的,联合与枚举结合并包装成结构体可用于实现带标签的联合。一个实际用途是实现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;
}

@bobobobo代码是正确的,正如@Joshua指出的那样(遗憾的是,我不允许添加注释,所以在这里做,IMO不允许它放在第一位的坏决定):

https://en.cppreference.com/w/cpp/language/data_members#Standard_layout告诉我们这样做是可以的,至少从c++ 14开始是这样

在具有非并集类类型T1的活动成员的标准布局联合中,允许读取另一个非并集类类型T2的联合成员的非静态数据成员m,前提是m是T1和T2的公共初始序列的一部分(除非通过非易失性glvalue读取易失性成员是未定义的)。

因为在当前的情况下T1和T2无论如何都提供了相同的类型。