ChatGPT解决这个技术问题 Extra ChatGPT

和 C++ 中联合的用途

我之前很舒服地使用了工会;今天当我阅读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

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

更新:

我想在事后澄清一些事情。

对于 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++ 标准允许,但 it wasn't the purpose or the original intention of introducing inheritance as a C++ language feature。这就是安德烈的答案继续被接受的原因。

简单地说,允许编译器在结构中的元素之间插入填充。因此,b, g, r,a 可能不连续,因此与 uint32_t 的布局不匹配。这是其他人指出的 Endianess 问题的补充。
这正是您不应该标记问题 C 和 C++ 的原因。答案是不同的,但由于回答者甚至不告诉他们正在回答什么标签(他们甚至知道吗?),你会得到垃圾。
@downvoter 感谢您不解释,我知道您希望我神奇地理解您的抱怨,并且以后不再重复:P
关于拥有联合的初衷,请记住,C 标准将 C 联合晚了几年。快速浏览一下 Unix V7 显示了一些通过联合进行的类型转换。
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 ...真的吗?你引用了一个例外note,而不是段落开头的要点“在一个联合中,最多有一个非静态数据成员可以随时激活,即在任何时候最多可以将一个非静态数据成员的值存储在一个联合中。”——下至p4:“一般来说,必须使用显式的析构函数调用和放置新的运算符来更改联合的活动成员"

J
Juan Carlos Ramirez

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

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

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

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

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

编辑: 在 C99 标准的技术勘误之一中给出了更详细的定义(参见 DR#257 和 { 2})。但是,请记住,形式上这并不能保护您免于尝试读取陷阱表示而陷入未定义的行为。


+1 详细说明,给出一个简单的实际例子并谈论工会的遗产!
我对这个答案的问题是我见过的大多数操作系统都有头文件可以做这个确切的事情。例如,我在 Windows 和 Unix 上的旧(64 位之前)版本的 <time.h> 中看到了它。如果我将被要求理解以这种确切方式工作的代码,那么将其视为“无效”和“未定义”是不够的。
@AndreyT“直到最近才使用联合进行类型双关语从来都不是合法的”:2004 年并不是“最近”,特别是考虑到最初措辞笨拙的只有 C99,似乎通过未定义的联合进行类型双关。实际上,尽管工会的类型双关在 C89 中是合法的,在 C11 中是合法的,并且在 C99 中一直是合法的,尽管直到 2004 年委员会才修复了不正确的措辞,并随后发布了 TC3。 open-std.org/jtc1/sc22/wg14/www/docs/dr_283.htm
@legends2k 编程语言由标准定义。 C99 标准的技术勘误 3 在其脚注 82 中明确允许类型双关语,我邀请您自己阅读。这不是在电视上采访摇滚明星并表达他们对气候变化的看法。 Stroustrup 的观点对 C 标准所说的内容的影响为零。
@legends2k“我知道任何人的意见都无关紧要,只有标准才重要”编译器作者的意见比(极差的)语言“规范”重要得多。
E
Erich Kitzmueller

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

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

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

我完全同意,在没有进入未定义行为混乱的情况下,也许这是我能想到的工会的最佳预期行为;但是当我只是使用时不会浪费空间,比如 intchar* 用于 10 个对象 [];在这种情况下,我实际上可以为每种数据类型而不是 VAROBJECT 声明单独的结构?它不会减少混乱并使用更少的空间吗?
传说:在某些情况下,你根本无法做到这一点。当你在 Java 中使用 Object 时,你在 C 中使用类似 VAROBJECT 的东西。
正如您所解释的,tagged unions 的数据结构似乎是联合的唯一合法用途。
还要举例说明如何使用这些值。
@CiroSantilli新疆改造中心六四事件轮法功C++ Primer中的一个例子可能会有所帮助。 wandbox.org/permlink/cFSrXyG02vOSdBk2
D
David Rodríguez - dribeas

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

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

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


+1 以获得清晰简单的回复!我同意,为了可移植性,您在第 2 段中给出的方法很好;但是我可以使用我在问题中提出的方式吗,如果我的代码被绑定到一个单一的架构(付出了可移植性的代价),因为它为每个像素值节省了 4 个字节,并且在运行该函数时节省了一些时间?
@legends2k,问题是优化器可能会假设 uint32_t 不会通过写入 uint8_t 来修改,因此当优化使用该假设时您会得到错误的值... @Joe,一旦您访问未定义的行为就会出现指针(我知道,有一些例外)。
@legends2k/AProgrammer: reinterpret_cast 的结果是实现定义的。使用返回的指针不会导致未定义的行为,只会导致实现定义的行为。换句话说,行为必须是一致的和定义的,但它是不可移植的。
@legends2k:任何体面的优化器都会识别选择整个字节并生成代码来读取/写入字节的按位操作,与联合相同但定义明确(且可移植)。例如 uint8_t getRed() const { 返回颜色 & 0x000000FF; } void setRed(uint8_t r) { color = (color & ~0x000000FF) | r; }
@curiousguy:标准规定,如果 sizeof (sometype) 报告 N,则将指向该类型的指针转换为 char* 并读取 N 值将产生一些(不一定是唯一的)无符号 char 值序列。它还指定用这样的 char 值序列覆盖对象会将其值设置为会产生该序列的值。标准可以指定一个联合的行为就像它拥有一系列无符号字符值一样,并且读取和写入的影响将根据对这些值的影响来定义。
b
bobobobo

我经常遇到的 union常见的用法是aliasing

考虑以下:

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

这是做什么的?它允许通过 either 名称对 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。


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

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

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


你有没有这样使用它(就像在问题中一样)? :)
这有点迂腐,但我不太接受“变体记录”。也就是说,我确信他们是在考虑之中,但如果他们是优先事项,为什么不提供他们呢? “提供构建块,因为它可能对构建其他东西也有用”只是直观地看起来更有可能。尤其是考虑到至少还有一个可能想到的应用程序 - 内存映射 I/O 寄存器,其中输入和输出寄存器(虽然重叠)是具有自己名称、类型等的不同实体。
@Stev314 如果这是他们想到的用途,他们本可以让它不是未定义的行为。
@Neil:+1 是第一个在没有遇到未定义行为的情况下说出实际使用情况的人。我猜他们可以像其他类型的双关语操作(reinterpret_cast 等)一样定义它的实现。但就像我问的那样,你用它来打双关吗?
@Neil - 内存映射寄存器示例不是未定义的,通常的字节序/等除外,并给出了“易失性”标志。在此模型中写入地址不会引用与读取相同地址相同的寄存器。因此,没有“你在读什么”的问题,因为你没有读回——无论你写到那个地址的任何输出,当你读的时候,你只是在读一个独立的输入。唯一的问题是确保您读取并集的输入端并写入输出端。在嵌入式东西中很常见 - 可能仍然是。
T
Totonga

在 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 memory 的时候,这个结构使用的内存比拥有所有成员的结构要少。

顺便说一句,C 提供

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

访问位值。


尽管您的两个示例都在标准中得到了完美定义;但是,嘿,使用位字段肯定会拍摄不可移植的代码,不是吗?
不,不是。据我所知,它得到了广泛的支持。
编译器支持不会转化为可移植的。 The C BookC(因此是 C++)不保证机器字中字段的顺序,所以如果你出于后一个原因使用它们,你的程序不仅是非可移植的,它也将依赖于编译器。
P
Paul R

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


不存在字节序问题吗?与“未定义”相比,这是一个相对简单的修复,但如果是这样的话,对于某些项目来说值得考虑。
M
Matthieu M.

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

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


N
Nick

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

此外,联合不只是 是为了节省空间。它们可以帮助现代编译器进行类型双关。如果您reinterpret_cast<> 编译器无法对您在做什么做出假设。它可能不得不丢弃它所知道的关于您的类型的信息并重新开始(强制写回内存,与 CPU 时钟速度相比,这在当今是非常低效的)。


J
JoeG

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


“从一种类型到另一种类型的 reinterpret_cast,其结果是实现定义的。”不它不是。实现不必定义它,而且大多数都没有定义它。此外,将一些随机值转换为指针的允许实现定义的行为是什么?
C
Cubbi

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


p
philcolbourn

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

我读到的问题是,由于变量的内存是共享的,因此通过写入一个变量,其他变量会发生变化,并且根据它们的类型,值可能毫无意义。

例如。联合{浮动f;诠释我; } X;

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

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

例如。联合{ char c[4];诠释我; } X;

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


必须字对齐的字节?这是没有意义的。根据定义,一个字节没有对齐要求。
是的,我可能应该使用一个更好的例子。谢谢。
@curiousguy:在很多情况下,人们可能希望字节数组是字对齐的。如果一个人有许多例如 1024 字节的数组,并且经常希望将一个数组复制到另一个数组,那么在许多系统上让它们字对齐可能会使 memcpy() 从一个到另一个的速度加倍。出于这个原因和其他原因,某些系统可能会推测性地调整 发生在结构/联合之外的char[]分配。在现有示例中,i 将与 c[] 的所有元素重叠的假设是不可移植的,但这是因为不能保证 sizeof(int)==4
s
supercat

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

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


欣赏历史课,但是使用定义诸如 undefined 之类的标准,在过去的 C 时代,K&R 书是唯一的“标准”,情况并非如此,必须确保不要将其用于任何目的,并且进入UB土地。
@legends2k:编写标准时,大多数 C 实现都以相同的方式处理联合,这种处理很有用。然而,一些人没有这样做,标准的作者不愿意将任何现有的实现标记为“不合格”。相反,他们认为,如果实施者不需要标准来告诉他们做某事(正如他们已经在做的事实所证明的那样),那么不指定或不定义只会保持现状。它应该使事物的定义不如编写标准之前的定义......
...似乎是最近的一项创新。所有这一切特别令人难过的是,如果针对高端应用程序的编译器编写者要弄清楚如何为 1990 年代大多数编译器实现的语言添加有用的优化指令,而不是破坏“只有“ 90% 的实现,其结果将是一种比超现代 C 表现更好、更可靠的语言。
K
Kotauskas

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

M
Mr. Boy

您可以使用 aa union 有两个主要原因:

一种以不同方式访问相同数据的便捷方式,例如在您的示例中

实际上更像是一种 C 风格的 hack,在您了解目标系统的内存架构如何工作的基础上,可以快捷地编写代码。如前所述,如果您实际上不针对许多不同的平台,通常可以侥幸逃脱。我相信一些编译器也可能让你使用打包指令(我知道他们在结构上做)?

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


r
rob

@bobobobo 代码是正确的,正如@Joshua 指出的那样(遗憾的是我不允许添加评论,所以在这里这样做,IMO 错误的决定首先禁止它):

https://en.cppreference.com/w/cpp/language/data_members#Standard_layout 表明这样做是可以的,至少从 C++14 开始

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

因为在当前情况下,T1 和 T2 无论如何都捐赠相同的类型。


关注公众号,不定期副业成功案例分享
关注公众号

不定期副业成功案例分享

领先一步获取最新的外包任务吗?

立即订阅