C++ “volatile”是否允许与联合使用类型双关语?

C++ “volatile”是否允许与联合使用类型双关语?,c++,volatile,unions,type-punning,C++,Volatile,Unions,Type Punning,我们都知道这种双关语 union U {float a; int b;}; U u; std::memset(u, 0, sizeof u); u.a = 1.0f; std::cout << u.b; 现在它变得定义良好,因为这种类型双关是允许的。 另外,正如您所看到的,u调用memcpy()后,内存保持不变。 现在,让我们添加线程和volatile关键字 union U {float a; int b;}; volatile U u; std::memset(u, 0,

我们都知道这种双关语

union U {float a; int b;};

U u;
std::memset(u, 0, sizeof u);
u.a = 1.0f;
std::cout << u.b;
现在它变得定义良好,因为这种类型双关是允许的。 另外,正如您所看到的,
u
调用
memcpy()
后,内存保持不变。

现在,让我们添加线程和
volatile
关键字

union U {float a; int b;};

volatile U u;
std::memset(u, 0, sizeof u);
u.a = 1.0f;

std::thread th([&]
{
    char *ptr = new char[sizeof u];
    std::memcpy(ptr, &u.a, sizeof u);
    std::memcpy(&u.b, ptr, sizeof u);
});
th.join();

std::cout << u.b;
没有其他线程但编译器不知道没有其他线程

从编译器的角度来看,没有任何改变!如果第三个示例定义良好,那么最后一个示例也必须定义良好

我们不需要第二个线程,因为它不会改变
u
内存。
若使用了
volatile
,编译器假定
u
可以在任何时候进行静默修改。在这种修改下,任何字段都可以激活

所以,编译器永远无法跟踪volatile union的哪个字段处于活动状态。 它不能假设一个字段在分配给之后仍然处于活动状态(而其他字段仍然处于非活动状态),即使没有任何东西真正修改该联合

所以,在最后两个例子中,编译器将给出
1.0f
转换为
int
的精确位表示。

问题是:我的推理正确吗?第三和第四个例子真的很好吗?标准对此有何规定?

否-您的推理是错误的。易失性部分是一个普遍的误解-易失性没有按照您的状态工作

工会部分也是错误的。读这个

C++(11),只有当最后一次写入对应下一次读时,才能期望正确的/明确定义的行为。p> 在实际代码中,第二个线程可以通过任何蹩脚的线程库实现,编译器可能不知道第二个线程。但由于volatile关键字,它仍然是定义良好的

这种说法是错误的,因此你得出结论所依据的其他逻辑是不正确的

假设您有如下代码:

int* currentBuf = bufferStart;
while(currentBuf < bufferEnd)
{
    *currentBuf = foobar;    
    currentBuf++;
}
如果
foobar
volatile
,则禁用此优化和许多其他代码生成优化。注意,我说的是代码生成。但是,只要不违反CPU的内存模型,CPU完全有权将读写移动到其核心内容

特别是,每次读取和写入
foobar
时,编译器不需要强制CPU返回主存需要做的就是避免某些优化。(严格来说,这并不是真的;编译器还必须确保保留某些涉及跳远的属性,以及一些与线程无关的其他次要细节。)如果有两个线程,且每个线程位于不同的处理器上,且每个处理器具有不同的缓存,
volatile
不要求缓存一致,如果它们都包含
foobar
的内存副本

一些编译器可能会为了您的方便而选择实现这些语义,但并不要求它们这样做;请查阅编译器文档

我注意到C#和Java确实需要volatile上的acquire和release语义,但这些需求可能非常弱。特别是,x86不会对两次易失性写入或两次易失性读取进行重新排序,而是允许在对另一个变量进行易失性写入之前对一个变量的易失性读取进行重新排序,事实上,在极少数情况下,x86处理器可以这样做。(请参阅以C#编写的一个谜题,该谜题说明了即使所有东西都是易变的并且具有acquire-release语义,低锁代码也可能是错误的。)


寓意是:即使您的编译器很有帮助,并且确实对易失性变量(如C#或Java)施加了额外的语义,也可能存在这样的情况,即在所有线程之间没有一致观察到的读写序列;许多内存模型都没有提出这一要求。这可能会导致奇怪的运行时行为。同样,如果您想知道
volatile
对您意味着什么,请参阅编译器文档。

volatile
与线程无关。首先,在第四个示例中,在没有同步的情况下读取变量,因此编译器可以假定没有其他线程写入它
volatile
仅表示变量上的每个操作都是可观察到的副作用;这与多线程执行无关。所以这个例子肯定是错误的。第二:你为什么要这么做?@BaummitAugen 1。我这样做是因为它看起来像一个黑客在简单的类型双关在C++中。2.您的意思是当
编译器
看到
th.join()
时,它假设voltaile变量有可能被修改?但是,如果我使用任何非标准线程库,例如SDL线程,该怎么办?编译器不知道
SDL\u WaitThread()
加入线程。3.
volatile仅表示变量上的每个操作都是可观察到的副作用
您能解释一下吗?我不明白你的意思。@HolyBlackCat它不需要知道,因为它知道你通过引用或其地址将变量传递给某个函数,否则新线程无法修改它。因此可以推断它可能已经改变了。此外,第二个示例似乎也错了,因为您正在读取未初始化的字节if
sizeof(float)
union U {float a; int b;};

volatile U u;
std::memset(u, 0, sizeof u);
u.a = 1.0f;
std::cout << u.b;
int* currentBuf = bufferStart;
while(currentBuf < bufferEnd)
{
    *currentBuf = foobar;    
    currentBuf++;
}
int* currentBuf = bufferStart;
int temp = foobar;
while(currentBuf < bufferEnd)
{
    *currentBuf = temp;    
    currentBuf++;
}