这永远不会是合法的,无论你用奇怪的演员和工会等等进行什么样的扭曲。
基本事实是:两个不同类型的对象可能永远不会在内存中出现别名,但有一些特殊例外(见下文)。
例子
考虑以下代码:
void sum(double& out, float* in, int count) {
for(int i = 0; i < count; ++i) {
out += *in++;
}
}
让我们将其分解为本地寄存器变量以更紧密地模拟实际执行:
void sum(double& out, float* in, int count) {
for(int i = 0; i < count; ++i) {
register double out_val = out; // (1)
register double in_val = *in; // (2)
register double tmp = out_val + in_val;
out = tmp; // (3)
in++;
}
}
假设 (1)、(2) 和 (3) 分别表示内存读取、读取和写入,在如此紧密的内部循环中,这可能是非常昂贵的操作。此循环的合理优化如下:
void sum(double& out, float* in, int count) {
register double tmp = out; // (1)
for(int i = 0; i < count; ++i) {
register double in_val = *in; // (2)
tmp = tmp + in_val;
in++;
}
out = tmp; // (3)
}
这种优化将所需的内存读取次数减少了一半,将内存写入次数减少到 1。这会对代码的性能产生巨大影响,对于所有优化的 C 和 C++ 编译器来说都是非常重要的优化。
现在,假设我们没有严格的别名。假设写入任何类型的对象都会影响任何其他对象。假设写入双精度值会影响某处浮点数的值。这使得上述优化变得可疑,因为程序员实际上可能打算将 out 和 in 设置为别名,从而使 sum 函数的结果更加复杂并受到过程的影响。听起来很愚蠢?即便如此,编译器也无法区分“愚蠢”和“聪明”的代码。编译器只能区分格式正确和格式错误的代码。如果我们允许自由别名,那么编译器必须在其优化中保持保守,并且必须在循环的每次迭代中执行额外的存储 (3)。
希望你现在能明白为什么没有这样的联合或演员戏法可能是合法的。你不能通过诡计来规避这样的基本概念。
严格别名的例外
char
C 和 C++ 标准为使用和任何“相关类型”(其中包括派生类型和基类型以及成员)为任何类型起别名做出了特殊规定,因为能够独立使用类成员的地址非常重要。您可以在此答案中找到这些条款的详尽列表。
此外,GCC 为从与上次写入内容不同的联合成员读取数据做了特殊规定。请注意,这种通过联合进行的转换实际上不允许您违反别名。任何时候都只允许一个联合的成员处于活动状态,因此例如,即使使用 GCC,以下行为也是未定义的:
union {
double d;
float f[2];
};
f[0] = 3.0f;
f[1] = 5.0f;
sum(d, f, 2); // UB: attempt to treat two members of
// a union as simultaneously active
解决方法
将一个对象的位重新解释为某种其他类型对象的位的唯一标准方法是使用memcpy
. 这利用了char
对象别名的特殊规定,实际上允许您在字节级别读取和修改底层对象表示。例如,以下是合法的,并且不违反严格的别名规则:
int a[2];
double d;
static_assert(sizeof(a) == sizeof(d));
memcpy(a, &d, sizeof(d));
这在语义上等同于以下代码:
int a[2];
double d;
static_assert(sizeof(a) == sizeof(d));
for(size_t i = 0; i < sizeof(a); ++i)
((char*)a)[i] = ((char*)&d)[i];
GCC 规定从不活跃的工会成员读取数据,隐含地使其活跃。从GCC 文档:
从不同的工会成员那里阅读而不是最近写入的成员(称为“类型双关语”)的做法很常见。即使使用 -fstrict-aliasing,也允许使用类型双关语,前提是通过联合类型访问内存。因此,上面的代码将按预期工作。请参阅结构联合枚举和位域实现。但是,此代码可能不会:
int f() {
union a_union t;
int* ip;
t.d = 3.0;
ip = &t.i;
return *ip;
}
类似地,通过获取地址、转换结果指针和取消引用结果的访问具有未定义的行为,即使转换使用联合类型,例如:
int f() {
double d = 3.0;
return ((union a_union *) &d)->i;
}
新的展示位置
(注意:我在这里凭记忆进行,因为我现在无法访问标准)。一旦将一个对象放置到存储缓冲区中,底层存储对象的生命周期就会隐式结束。这类似于您给工会成员写信时发生的情况:
union {
int i;
float f;
} u;
// No member of u is active. Neither i nor f refer to an lvalue of any type.
u.i = 5;
// The member u.i is now active, and there exists an lvalue (object)
// of type int with the value 5. No float object exists.
u.f = 5.0f;
// The member u.i is no longer active,
// as its lifetime has ended with the assignment.
// The member u.f is now active, and there exists an lvalue (object)
// of type float with the value 5.0f. No int object exists.
现在,让我们看一下与placement-new类似的东西:
#define MAX_(x, y) ((x) > (y) ? (x) : (y))
// new returns suitably aligned memory
char* buffer = new char[MAX_(sizeof(int), sizeof(float))];
// Currently, only char objects exist in the buffer.
new (buffer) int(5);
// An object of type int has been constructed in the memory pointed to by buffer,
// implicitly ending the lifetime of the underlying storage objects.
new (buffer) float(5.0f);
// An object of type int has been constructed in the memory pointed to by buffer,
// implicitly ending the lifetime of the int object that previously occupied the same memory.
出于显而易见的原因,这种隐式的生命周期结束只会发生在具有微不足道的构造函数和析构函数的类型中。