此代码的行为是否定义明确?
#include <stdatomic.h>
const int test = 42;
const int * _Atomic atomic_int_ptr;
atomic_init(&atomic_int_ptr, &test);
const int ** int_ptr_ptr = &atomic_int_ptr;
printf("int = %d\n", **int_ptr_ptr); //prints int = 42
我将原子类型的指针分配给非原子类型的指针(类型相同)。这是我对这个示例的看法:
该标准明确规定了const
,volatile
和restrict
限定词与_Atomic
限定词6.2.5(p27)
的区别:
本标准明确使用短语“原子的,合格的或 非限定类型”,只要允许使用该类型的原子版本 以及其他类型的合格版本。词组 “'合格或不合格类型'”,但没有具体提及原子, 不包括原子类型。
还将限定类型的兼容性定义为6.7.3(p10)
:
要使两个合格的类型兼容,两个都应具有 兼容类型的完全相同的版本;的顺序 限定符或限定符列表中的类型限定符 不影响指定的类型。
结合以上引用,我得出结论,原子类型和非原子类型是兼容的类型。因此,应用简单分配规则6.5.16.1(p1)
(临时矿):
左操作数具有原子,合格或不合格的指针 类型,以及(考虑到左操作数将具有的类型 左值转换后)两个操作数都是限定 的指针 兼容类型的 或不合格版本,以及由 左侧具有右侧所指类型的所有限定词;
因此,我得出的结论是,行为已得到很好的定义(即使将原子类型分配为非原子类型也是如此)。
所有问题是应用上述规则,我们还可以得出结论: 将非原子类型简单分配给原子类型 显然不正确,因为我们有专用的泛型atomic_store
函数。
答案 0 :(得分:8)
此外,还有_Atomic限定词。 _Atomic的存在 限定词指定原子类型。大小,表示形式和 原子类型的对齐方式不必与原子类型的对齐方式相同 对应的不合格类型。因此,本标准明确 无论何时,只要使用“原子,合格或不合格类型”一词, 一个类型的原子版本与其他合格版本一起被允许 类型的版本。短语“合格或不合格类型”, 没有具体提及原子,不包括原子类型。
我认为这应该明确表明,原子限定的类型与它们所基于的类型的限定或不限定版本不兼容。
答案 1 :(得分:6)
C11允许_Atomic T
具有与T
不同的大小和布局,例如如果它不是无锁的。 (请参阅@PSkocik的答案)。
例如,实现可以选择将互斥锁放在每个原子对象中,然后将其放在最前面。 (大多数实现将地址用作锁表的索引:Where is the lock for a std::atomic?而不是膨胀_Atomic
或std::atomic<T>
对象的每个实例,这些对象不能保证在编译时没有锁时间)。
因此,即使在单线程程序中,_Atomic T*
也与T*
不兼容。
仅分配一个指针可能不是UB (很抱歉,我没有戴上语言律师的帽子),但取消引用当然可以。
我不确定_Atomic T
和T
确实共享相同的布局和对齐方式的实施是否严格意义上的UB。如果_Atomic T
和T
被认为是不同的类型,而不论它们是否共享相同的布局,则可能会违反严格的别名。
alignof(T)
可能与alignof(_Atomic T)
不同,但除了故意的不正当实施方式(Deathstation 9000)以外,_Atomic T
至少与普通对齐T
,因此将指针转换为已经存在的对象不是问题。对齐程度比实际需要的对象不是问题,如果它阻止编译器使用单个更宽的负载,则可能是未命中的优化。
有趣的事实:在ISO C中,即使未取消引用,创建未对齐的指针也是UB。 (大多数实现都不会抱怨,英特尔的_mm_loadu_si128
内在函数甚至需要编译器来支持。)
在实际实现中,_Atomic T*
和T*
使用相同的布局/对象表示形式和alignof(_Atomic T) >= alignof(T)
。如果可以解决严格混淆的UB,则程序的单线程或互斥锁保护的部分可以对_Atomic
对象进行非原子访问。也许和memcpy
在一起。
在实际的实现中,_Atomic
可能会增加对齐要求,例如对于大多数64位ISA,大多数ABI上的struct {int a,b;}
通常仅具有4字节对齐方式(最多成员数),但是_Atomic
将使其自然对齐方式= 8以允许使用以下方式加载/存储它单个对齐的64位加载/存储。当然,这不会改变成员相对于对象起点的布局或对齐方式,而只会更改整个对象的对齐方式。
所有问题的是,应用上述规则,我们还可以得出结论:将非原子类型简单分配给原子类型也定义得很好,这显然是不正确的,因为我们有专门的泛型atomic_store函数。
不,这种推理是有缺陷的。
atomic_store(&my_atomic, 1)
等效于my_atomic=1;
。在C抽象机中,它们都使用memory_order_seq_cst
进行原子存储。
通过查看任何ISA上实际编译器的代码源,您还可以看到这一点;例如x86编译器将使用xchg
指令或mov
+ mfence
。同样,shared_var++
编译为原子RMW(带有mo_seq_cst
)。
IDK为什么会有atomic_store
泛型函数。也许只是为了与atomic_store_explicit
形成对比/一致性,它使您可以atomic_store_explicit(&shared_var, 1, memory_order_release)
或memory_order_relaxed
进行发布或放松存储,而不是顺序发布。 (在x86上,这只是一个普通的存储。或者在顺序较弱的ISA上,有一定的栅栏,但没有完整的屏障。)
对于无锁类型,_Atomic T
和T
的对象表示相同,在实践中通过非原子指针访问原子对象没有问题。单线程程序。我怀疑它仍然是UB。
C ++ 20计划引入std::atomic_ref<T>
,它将使您可以对非原子变量执行原子操作。 (没有UB,只要在编写的时间段内没有线程潜在地对其进行非原子访问。)例如,这基本上是围绕GCC中__atomic_*
内置函数的包装,例如,{{1} }的实现。
(这会带来一些问题,例如如果std::atomic<T>
比atomic<T>
需要更多的对齐方式,例如i386系统V上的T
或long long
。或2x { {1}}在大多数64位ISA上。在声明要对其执行原子操作的非原子对象时,应使用double
。
无论如何,我不知道在便携式 ISO C11中执行类似操作的任何符合标准的方法,但是值得一提的是,真正的C编译器确实支持原子操作声明没有int
的对象上。但是only using stuff like GNU C atomic builtins.:
请参见Casting pointers to _Atomic pointers and _Atomic sizes:显然,即使在GNU C中,也不建议将alignas(_Atomic T) T foo
强制转换为_Atomic
。尽管我们没有确切的答案,实际上它是UB。