c++ - 严格的混叠和对齐

标签 c++ c++11 unions strict-aliasing type-punning

我需要一种在任意 POD 类型之间进行别名的安全方法,明确考虑到 n3242 或更高版本的 3.10/10 和 3.11,符合 ISO-C++11。 这里有很多关于严格别名的问题,其中大部分是关于 C 而不是 C++。我找到了一个使用 union 的 C 的“解决方案”,可能使用本节

union type that includes one of the aforementioned types among its elements or nonstatic data members

我由此构建了这个。

#include <iostream>

template <typename T, typename U>
T& access_as(U* p)
{
    union dummy_union
    {
        U dummy;
        T destination;
    };

    dummy_union* u = (dummy_union*)p;

    return u->destination;
}

struct test
{
    short s;
    int i;
};

int main()
{
    int buf[2];

    static_assert(sizeof(buf) >= sizeof(double), "");
    static_assert(sizeof(buf) >= sizeof(test), "");

    access_as<double>(buf) = 42.1337;
    std::cout << access_as<double>(buf) << '\n';

    access_as<test>(buf).s = 42;
    access_as<test>(buf).i = 1234;

    std::cout << access_as<test>(buf).s << '\n';
    std::cout << access_as<test>(buf).i << '\n';
}

我的问题是,为了确定,这个程序按照标准合法吗?*

在使用 MinGW/GCC 4.6.2 进行编译时,它不会发出任何警告并且工作正常:

g++ -std=c++0x -Wall -Wextra -O3 -fstrict-aliasing -o alias.exe alias.cpp

* 编辑:如果不是,怎么可能修改为合法?

最佳答案

这永远不会是合法的,无论你用奇怪的 Actor 和 union 等等进行什么样的扭曲。

基本事实是:两个不同类型的对象可能永远不会在内存中出现别名,除了一些特殊异常(exception)(请参阅下文)。

示例

考虑以下代码:

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++ 编译器来说都是非常重要的优化。

现在,假设我们没有严格的别名。假设写入任何类型的对象都会影响任何其他对象。假设写入 double 值会影响某处 float 的值。这使得上述优化变得可疑,因为程序员实际上可能打算将 out 和 in 设置为别名,从而使 sum 函数的结果更加复杂并受到过程的影响。听起来很愚蠢?即便如此,编译器也无法区分“愚蠢”和“聪明”的代码。编译器只能区分格式正确和格式错误的代码。如果我们允许自由别名,那么编译器必须在其优化中保持保守,并且必须在循环的每次迭代中执行额外的存储 (3)。

希望您现在能明白为什么没有这样的 union 或施法技巧可能是合法的。你不能通过花招来规避这样的基本概念。

严格别名的异常(exception)情况

C 和 C++ 标准为使用 char 和任何“相关类型”(其中包括派生类型和基类型以及成员)为任何类型设置别名,因为能够使用独立的类(class)成员的地址是如此重要。您可以在 this answer. 中找到这些规定的详尽列表。

此外,GCC 为从与上次写入内容不同的 union 成员读取数据做出了特殊规定。请注意,这种通过 union 进行的转换实际上不允许您违反别名。任何时候都只允许一个 union 的成员处于事件状态,因此例如,即使使用 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 对象的别名的特殊规定,实际上允许您在字节级别读取和修改底层 object 表示。例如,以下是合法的,并且不违反严格的别名规则:

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 规定从不活动的 union 成员读取数据,隐含地使其处于事件状态。来自 GCC documentation:

The practice of reading from a different union member than the one most recently written to (called “type-punning”) is common. Even with -fstrict-aliasing, type-punning is allowed, provided the memory is accessed through the union type. So, the code above will work as expected. See Structures unions enumerations and bit-fields implementation. However, this code might not:

int f() {
    union a_union t;
    int* ip;
    t.d = 3.0;
    ip = &t.i;
    return *ip;
}

Similarly, access by taking the address, casting the resulting pointer and dereferencing the result has undefined behavior, even if the cast uses a union type, e.g.:

int f() {
    double d = 3.0;
    return ((union a_union *) &d)->i;
} 

放置新

(注意:我在这里凭内存进行,因为我现在无法访问标准)。 一旦将一个对象放置到存储缓冲区中,底层存储对象的生命周期就会隐式结束。这类似于您给 union 成员写信时发生的情况:

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.

出于显而易见的原因,这种隐式的生命周期结束只会发生在具有微不足道的构造函数和析构函数的类型中。

关于c++ - 严格的混叠和对齐,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/9964418/

相关文章:

C++ 未定义的命名空间

c++ - C++线程创建/删除与线程停止/恢复

c++ - 如何在单击时显示 X、Y 坐标并在控制台中显示

c++ - constexpr 构造函数的参数类型 'std::function' 不是文字类型

c++ - Union hack 用于字节序测试和字节交换

c - 为什么 union 适用于 C 语言的低级系统编程?

c - Union - 二进制到 double

c++ - 当通知简历时,condition_variable::wait_for是否返回true

c++ - 我什么时候应该 `#include <ios>` , `#include <iomanip>` 等?

c++ - std::function 在堆栈数组中使用时崩溃