C/C++联合体和未定义行为

8
以下情况是否属于未定义行为?
 union {
   int foo;
   float bar;
 } baz;

 baz.foo = 3.14 * baz.bar;

我记得在两个序列点之间从同一基础内存中写入和读取数据是未定义的行为,但我不确定。

评估是无序的,但副作用不是,在C++11中是有序的。 - Kerrek SB
4
你需要翻译的内容是:“Which language do you want an answer for?”我的回答是:“你希望得到哪种语言的答案?” - Alan Stokes
@AlanStokes:标签为C和C++ :D - Vroomfondel
5
请明确您需要C语言还是C++语言的答案。如果两者都需要,那么这个问题就太广泛了。 - Walter
@Walter,为什么SO允许使用这两个标签? - curiousguy
4个回答

6
我记得在两个序列点之间读写同一块底层内存是未定义行为,但我不确定。
只有当在两个序列点之间该位置被修改超过一次或者副作用与在同一位置使用值的值计算无序时,才会在同一表达式中读写同一内存位置不会引起未定义行为。
C11: 6.5表达式:
如果标量对象上的副作用与同一标量对象上的不同副作用或者使用相同标量对象的值计算的值无序,则行为未定义。[...]
表达式
 baz.foo = 3.14 * baz.bar;  

如果在初始化之前定义了bar,则其行为是被明确定义的。原因是对baz.foo的副作用相对于对象baz.foobaz.bar的值计算进行了排序。

C11: 6.5.16/3 赋值运算符:

[...] 更新左操作数存储值的副作用在左右操作数的值计算后进行排序。操作数的评估是无序的。


1
同一内存位置的无序读写与两个无序写一样,都是未定义行为。 - Brian Bi
3
@melpomene,“unsequenced”是C2011标准中的一个定义术语。完整的定义可能在这个场合不太适合,但我鼓励你阅读标准本身对此的解释以及“先于”关系的说明。 - John Bollinger
3
请阅读此链接以了解“sequence before”和“unsequenced”的一切。你可以从这里下载n1570的pdf文档。 - haccks
2
@haccks,您忽略了与此问题相关的标准关键条款:“更新赋值操作符左侧存储值的副作用在左右操作数的值计算之后进行排序”(引自C11 6.5.16/3)。如果没有该条款或其他具有相同效果的规定,则您引用的规定将认为该行为是未定义的。 - John Bollinger
1
我认为在历史上有这样一个要求,即对象只能在同一表达式中读取和写入,如果两者以相同的方式访问。当然,在许多机器上,这样的规则可以启用有用的优化(例如,在具有两个数据指针的8x51克隆上,给定“uint32_t foo,bar;”从“foo”复制四个字节到“bar”的最有效实现是通过将第一个字节从foo复制到bar的第一个字节,然后第二个字节、第三个字节和第四个字节,但如果它们重叠可能会出现故障。) - supercat
显示剩余15条评论

4

声明:本回答涉及C++。

您正在访问一个尚未开始生命周期的对象 - baz.bar - 这会通过[basic.life]/(6.1)导致未定义行为。

假设bar已经被初始化,那么您的代码就没问题了;在赋值之前,foo不需要存活,因为没有执行依赖于其值的操作,在其期间,通过重用内存并有效地初始化它来更改活动成员。目前的规则对后者不太清楚,请参见CWG #1116。然而,现状是这样的,这种赋值确实将目标成员设置为活动(= 存活)。

请注意,赋值在操作数的值计算之后被排序(即保证发生),请参见[expr.ass]/1。


1
@Columbo 但这样不是很奇怪吗?u.a = u.b 是未定义的,但 u.a = B(u.b) 就可以了? - Barry
1
假设baz.bar已经初始化且baz.foo没有被写入,该行为在C中是明确定义的。鉴于2011年标准中C/C++协调努力的成果,我会非常惊讶地发现相同的代码在C++中具有未定义的行为。 - John Bollinger
@Columbo,我指的是你之前回答的版本,它与你现在的回答相反。我还没有收到更新。抱歉打扰了。 - John Bollinger
@JohnBollinger 对于我那些不够严谨的措辞导致大家感到困惑,我很抱歉。 :-) - Columbo
@curiousguy... 为了更有效地进行编程,我会熟悉标准术语。 - Columbo
显示剩余7条评论

2

回答关于 C 语言的问题,不是 C++

我原本认为这是定义行为,但后来我读到了以下段落,它来自于ISO C2x(我猜在旧的 C 标准中也存在,但没有检查):

6.5.16.1/3 (赋值运算符::简单赋值::语义):

如果将要存储在一个对象中的值从与第一个对象的存储有任何重叠的另一个对象中读取,则重叠必须是精确的,并且两个对象必须具有兼容类型的限定或非限定版本;否则,行为未定义。

因此,考虑以下情况:

union {
    int        a;
    const int  b;
} db;

union {
    int    a;
    float  b;
} ub1;

union {
    uint32_t  a;
    int32_t   b;
} ub2;

然后,执行以下操作是定义行为:
db.a = db.b + 1;

但是这样做是未定义的行为:

ub1.a = ub1.b + 1;

或者

ub2.a = ub2.b + 1;

兼容类型的定义在6.2.7/1(兼容类型和组合类型)中。另请参见:__builtin_types_compatible_p()


虽然 + 运算符会创建一个新对象,但这是否意味着它重新定义了变量呢?例如,ub1.a = ub1.b; 明显是未定义行为,但 ub1.a = ub1.b + 1; 呢?我在编译该代码时收到了警告,但我并不确定。 - alx - recommends codidact
1
据我所知,标准中没有暗示内置运算符会产生其操作数的副本,在许多8位或16位平台上,让long1 = long2 + 1;执行中间复制将大大增加代码大小和执行时间。 - supercat
1
顺便提一下,在某些8位平台上,执行long1 = long2+long3;的最快方法是将代码处理为long1 = long2; long1+=long3;long1 = long3; long1+=long2;,但第一个替换仅在已知long1long3标识不同存储区域时才有效,而第二个替换仅在long1long2不同的情况下才有效。 - supercat
1
我为回答所付出的明显努力点赞了,但我认为它是错误的。ub1.a和ub1.b不会同时存在,因此没有两个重叠的对象,这使得该条款不适用。结合6.5.16/3(在读取之后更新的副作用),似乎使得所有三种情况都合法。 - Vroomfondel
@Vroomfondel:标准的意图是让编译器在分配源和目标共享存储空间的情况下采取必要措施,特别是标准所认可的方式,但不要求它们考虑对象可能以编译器没有特定原因预期的方式共享存储空间的可能性。人们忽视的一件事是,标准的设计旨在允许编译器编写人员尽可能地使其产品更有用。在某些平台上,维护行为保证可能很昂贵… - supercat
显示剩余11条评论

0
标准使用“未定义行为”这个短语,作为一种捕捉所有情况的方式,其中许多实现会以至少有些可预测的方式处理构造(例如产生一个不一定可预测的值而没有副作用),但标准的作者们认为试图预测实现可能做的一切是不切实际的。它并不意味着要求实现无端地表现得毫无意义,也不表示代码错误(短语“非可移植或错误”旨在包括可能在某些机器上失败但对于不打算与这些机器一起使用的代码是正确的构造)。
在一些平台上,比如8051,如果编译器给出了这样的结构:someInt16 += *someUnsignedCharPtr << 4;,如果不必考虑指针可能指向someInt16的低字节,最有效的处理方式是获取*someUnsignedCharPtr,将其左移四位,加到someInt16的LSB上(捕获进位),重新加载*someUnsignedCharPtr,将其右移四位,再加上之前的进位和someInt16的MSB。两次从*someUnsignedCharPtr中加载值比先加载它,将其值存储到临时位置后进行移位,然后再从该临时位置加载其值要快。但是,如果someUnsignedCharPtr指向someInt16的低字节,则在第二次加载someUnsignedCharPtr之前修改该低字节会破坏该字节的高位,这些高位在移位后会被加到someInt16的高字节上。
标准允许编译器生成这样的代码,即使字符指针不受别名规则的限制,因为它并不要求编译器处理所有情况,其中未排序的读写会影响部分重叠的存储区域。如果使用联合而不是字符指针执行此类访问,则编译器可能会认识到字符类型访问将始终重叠16位值的最低有效字节,但我认为标准的作者们不想要求编译器投入可能需要处理这些晦涩案例的时间和精力。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接