位域和序列点

17

如果将f0f1打包进同一个字节的实现方式,那么下面的程序是否被定义了呢?

struct S0 {
       unsigned f0:4;
       signed f1:4;
} l_62;

int main (void) {
       (l_62.f0 = 0) + (l_62.f1 = 0);
       return 0;
}

我对C99和C11的答案感兴趣,如果有理由认为它们不同,请提供各自的答案。

在C99中,我找到的是6.5:2:

在上一个序列点和下一个序列点之间,一个对象通过表达式的计算至多被修改一次其存储的值。 [...]

对于我来说,这段话对上面的程序有什么后果并不清楚。

基于大量的随机测试,大多数编译器似乎会生成代码,使得这两个赋值语句不会相互干扰。


1
这些标准的部分让我头疼。我的理解是,意图是它是未定义行为(例如,在C11中很明确,修改两个线程中的两个字段是不同步的),但是6.5节的语言忘记提到位域,就像其他处理位域的地方一样。 - AProgrammer
@AProgrammer:上面的代码具有明确的逻辑含义(将写入视为按顺序发生,以任意顺序),没有理由不得体的编译器不能在单线程情况下生成产生该行为的代码。你能否提出任何合理的理由,解释标准作者为什么会希望它成为未定义行为? - supercat
2个回答

3
C11将相邻的命名位域视为同一内存位置的一部分。这种位域不保证以原子方式更新,换句话说,如果一个更新在另一个更新之前没有明确排序,则行为是未定义的。3.14内存位置还详细解释了何时可以将两个字段视为不同的内存位置,因此可以将它们的更新视为独立的。
如果您修改结构体...
struct S0 {
       unsigned f0:4;
       int :0;
       signed f1:4;
} l_62;

如果两个位域之间有奇怪的“内存位置分隔符”,那么您的代码应该是可以保证正常工作的。

对于C99来说,情况似乎更加复杂,没有这样详细的内存位置概念。在Linux内核邮件列表的最近讨论中,有一个说法是,对于所有位域对,无论更新哪一个,都会保证原子性。该讨论的起点是GCC不按预期方式污染了相邻的非位域,导致出现错误崩溃。


2
在C11中,修改相同的内存位置是数据竞争,这是确定的。但是,虽然5.1.2.4/4提到了内存位置,但6.5/2只提到了标量对象。我不确定这是否是疏忽。在C++中,相应的措辞类似,对于单个线程中的单个表达式,存在数据竞争但显然不是UB。 - AProgrammer
@AProgrammer:在大多数平台上,强制要求对同一存储元素中的不同位域进行同时写入(不同线程)时按顺序一致的方式行为是极不切实际的。大多数平台可以以基本零成本保证一个字段的写入不会影响另一个字段的同时读取,但由于这在所有平台上都不是免费的,因此标准没有强制执行。然而,我认为没有什么迹象表明编译器不应该处理单线程情况。 - supercat

0

这里的任务是对结构体成员进行赋值。它们恰好共享相同的存储空间不应该影响逻辑。实际上,您并没有将其分配给相同的东西。

当然,我不是语言律师。


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