这段代码的行为是否定义明确?
#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)
(emp.mine)的规则:
左操作数具有原子,限定或非限定指针类型,并且(考虑左值操作数在左值转换后将具有的类型)两个操作数都是指向兼容类型的限定或非限定版本的指针,左侧指向的类型具有全部右边指出的那种限定词;
所以我得出结论,行为是明确定义的(即使将原子类型分配给非原子类型)。
所有这一切的问题在于应用上面的规则我们也可以得出结论,非原子类型到原子类型的简单分配也很明确,这显然不正确,因为我们有一个专用的通用atomic_store
函数。
此外,还有_Atomic限定符。 _Atomic限定符的存在指定原子类型。原子类型的大小,表示和对齐不必与相应的非限定类型的大小,表示和对齐相同。因此,只要允许类型的原子版本与类型的其他限定版本一起使用,本标准就明确使用短语“原子,限定或非限定类型”。没有特别提及原子的短语“'合格或不合格类型''不包括原子类型。
我认为这应该清楚地表明原子合格类型不被认为与它们所基于的类型的合格或不合格版本兼容。
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_*
内置的包装器,例如,std::atomic<T>
是在其上实现的。
(这提出了一些问题,比如atomic<T>
需要比T
更多的对齐,例如i3886 System V上的long long
或double
。或者大多数64位ISA上的2x int
结构。在声明你想要的非原子对象时应该使用alignas(_Atomic T) T foo
能够进行原子操作。)
无论如何,我不知道任何符合标准的方法在可移植的ISO C11中做类似的事情,但值得一提的是,真正的C编译器非常支持对没有_Atomic
声明的对象进行原子操作。但是only using stuff like GNU C atomic builtins.:
请参阅Casting pointers to _Atomic pointers and _Atomic sizes:即使在GNU C中也不建议将T*
投射到_Atomic T*
。虽然我们没有明确的答案,但它实际上是UB。