【发布时间】:2015-10-17 11:34:31
【问题描述】:
我们都知道这样的双关语
union U {float a; int b;};
U u;
std::memset(u, 0, sizeof u);
u.a = 1.0f;
std::cout << u.b;
在 C++ 中是未定义的行为。
它是未定义的,因为在 u.a = 1.0f; 赋值后,.a 变为活动字段,.b 变为非活动字段,从非活动字段读取是未定义的行为。我们都知道这一点。
现在,考虑以下代码
union U {float a; int b;};
U u;
std::memset(u, 0, sizeof u);
u.a = 1.0f;
char *ptr = new char[std::max(sizeof (int),sizeof (float))];
std::memcpy(ptr, &u.a, sizeof (float));
std::memcpy(&u.b, ptr, sizeof (int));
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;
逻辑保持不变,但我们只有第二个线程。由于volatile 关键字代码保持良好定义。
在实际代码中,第二个线程可以通过任何糟糕的线程库实现,编译器可能不知道第二个线程。但是由于volatile 关键字的存在,它仍然是明确定义的。
但是如果没有其他线程呢?
union U {float a; int b;};
volatile U u;
std::memset(u, 0, sizeof u);
u.a = 1.0f;
std::cout << u.b;
没有其他线程。 但是编译器不知道没有其他线程!
从编译器的角度来看,没有任何改变!如果第三个示例定义明确,那么最后一个示例也必须明确定义!
我们不需要第二个线程,因为它无论如何都不会改变u 内存。
如果使用
volatile,编译器假定u 可以在任何时候静默修改。在这样的修改中,任何字段都可以变为活动状态。
因此,编译器永远无法跟踪 volatile 联合的哪个字段处于活动状态。 它不能假设一个字段在分配给它之后仍然处于活动状态(并且其他字段仍然处于非活动状态),即使没有真正修改该联合。
因此,在最后两个示例中,编译器将给出1.0f 转换为int 的精确位表示。
问题是:我的推理是否正确?第 3 和第 4 个例子真的很好辩解吗?标准对此有何规定?
【问题讨论】:
-
volatile与线程无关。见Why does volatile exist? -
首先,在您的第四个示例中,您在没有同步的情况下读取了一个变量,因此编译器可以假设没有其他线程写入它。
volatile仅表示对变量的每个操作都是可观察到的副作用;这与多线程执行无关。所以这个例子肯定是错误的。第二:你为什么要做这样的事情? -
@BaummitAugen 1. 我这样做是因为它看起来像是在 C++ 中用于简单类型双关语的 hack。 2. 你的意思是当
compiler看到th.join()时,它会假设 voltaile 变量有可能被修改?但是,如果我使用任何非标准线程库,例如 SDL 线程,该怎么办?编译器不知道SDL_WaitThread()加入了一个线程。 3.volatile only means that every operation on the variable is a observable side effect你能解释一下吗?我不明白你的意思。 -
@HolyBlackCat 它不需要知道,因为它知道你通过引用或它的地址将变量传递给某个函数,否则新线程无法修改它。所以它可以推断它可能已经改变了。
-
此外,第二个示例似乎也是错误的,因为如果
sizeof(float) < sizeof(int)正在读取未初始化的字节。
标签: c++ volatile unions type-punning