严格的混叠和对齐

时间:2012-04-01 12:34:12

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

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

  

联合类型,其中包含上述类型之一   元素或非静态数据成员

由此我建立了这个。

#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

* 编辑:如果没有,怎么能修改这个是合法的?

4 个答案:

答案 0 :(得分:14)

这不会是合法的,不管你用怪异的演员和工会做什么样的扭曲等等。

基本事实是:两个不同类型的对象可能永远不会在内存中出现别名,但有一些特殊例外(请参见下文)。

实施例

请考虑以下代码:

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

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

希望你现在可以看到为什么没有这样的联盟或演员技巧可能是合法的。你无法通过手法来规避这样的基本概念。

严格别名的例外

C和C ++标准对使用char的任何类型以及任何“相关类型”(其中包括派生类型和基类型以及成员)进行别名的特殊规定,因为能够使用a的地址班级成员独立是如此重要。您可以在this answer.

中找到这些条款的详尽列表

此外,海湾合作委员会对工会的其他成员的阅读作出特别规定,而不是最后写的。请注意,这种转换通过联合实际上不允许您违反别名。任何时候只允许一个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对象进行别名的特殊规定,实际上允许您在字节级别读取和修改基础对象表示。例如,以下内容是合法的,并且不违反严格的别名规则:

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 documentation:

  

从不同的工会成员阅读的做法比最近写的那个(称为“打字式”)很常见。即使使用-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.

这种隐含的生命周期结束只能出现在具有普通构造函数和析构函数的类型中,原因很明显。

答案 1 :(得分:6)

除了sizeof(T) > sizeof(U)时出现的错误,可能存在的问题是,由于U,联合具有比T更合适且可能更高的对齐。 如果你没有实例化这个联合,那么它的内存块是对齐的(并且足够大!)然后获取目标类型为T的成员,它将在最坏的情况下静默地破坏。

例如,如果您执行U*的C样式转换,其中U需要4个字节对齐,dummy_union*,其中dummy_union,则会发生对齐错误需要对齐到8个字节,因为alignof(T) == 8。之后,您可能会读取类型为T的联合成员,对齐为4而不是8个字节。


Alias cast(仅针对POD的对齐和大小安全reinterpret_cast):

此提案明确违反严格别名,但使用静态断言:

///@brief Compile time checked reinterpret_cast where destAlign <= srcAlign && destSize <= srcSize
template<typename _TargetPtrType, typename _ArgType>
inline _TargetPtrType alias_cast(_ArgType* const ptr)
{
    //assert argument alignment at runtime in debug builds
    assert(uintptr_t(ptr) % alignof(_ArgType) == 0);

    typedef typename std::tr1::remove_pointer<_TargetPtrType>::type target_type;
    static_assert(std::tr1::is_pointer<_TargetPtrType>::value && std::tr1::is_pod<target_type>::value, "Target type must be a pointer to POD");
    static_assert(std::tr1::is_pod<_ArgType>::value, "Argument must point to POD");
    static_assert(std::tr1::is_const<_ArgType>::value ? std::tr1::is_const<target_type>::value : true, "const argument must be cast to const target type");
    static_assert(alignof(_ArgType) % alignof(target_type) == 0, "Target alignment must be <= source alignment");
    static_assert(sizeof(_ArgType) >= sizeof(target_type), "Target size must be <= source size");

    //reinterpret cast doesn't remove a const qualifier either
    return reinterpret_cast<_TargetPtrType>(ptr);
}

使用指针类型参数(如reinterpret_cast等标准强制转换运算符):

int* x = alias_cast<int*>(any_ptr);

另一种方法(使用临时联合避免对齐和别名问题):

template<typename ReturnType, typename ArgType>
inline ReturnType alias_value(const ArgType& x)
{
    //test argument alignment at runtime in debug builds
    assert(uintptr_t(&x) % alignof(ArgType) == 0);

    static_assert(!std::tr1::is_pointer<ReturnType>::value ? !std::tr1::is_const<ReturnType>::value : true, "Target type can't be a const value type");
    static_assert(std::tr1::is_pod<ReturnType>::value, "Target type must be POD");
    static_assert(std::tr1::is_pod<ArgType>::value, "Argument must be of POD type");

    //assure, that we don't read garbage
    static_assert(sizeof(ReturnType) <= sizeof(ArgType),"Target size must be <= argument size");

    union dummy_union
    {
        ArgType x;
        ReturnType r;
    };

    dummy_union dummy;
    dummy.x = x;

    return dummy.r;
}

用法:

struct characters
{
    char c[5];
};

//.....

characters chars;

chars.c[0] = 'a';
chars.c[1] = 'b';
chars.c[2] = 'c';
chars.c[3] = 'd';
chars.c[4] = '\0';

int r = alias_value<int>(chars);

这样做的缺点是,并集可能需要比ReturnType

实际需要的内存更多的内存

包装的memcpy(使用memcpy绕过对齐和别名问题):

template<typename ReturnType, typename ArgType>
inline ReturnType alias_value(const ArgType& x)
{
    //assert argument alignment at runtime in debug builds
    assert(uintptr_t(&x) % alignof(ArgType) == 0);

    static_assert(!std::tr1::is_pointer<ReturnType>::value ? !std::tr1::is_const<ReturnType>::value : true, "Target type can't be a const value type");
    static_assert(std::tr1::is_pod<ReturnType>::value, "Target type must be POD");
    static_assert(std::tr1::is_pod<ArgType>::value, "Argument must be of POD type");

    //assure, that we don't read garbage
    static_assert(sizeof(ReturnType) <= sizeof(ArgType),"Target size must be <= argument size");

    ReturnType r;
    memcpy(&r,&x,sizeof(ReturnType));

    return r;
}

对于任何POD类型的动态大小的数组:

template<typename ReturnType, typename ElementType>
ReturnType alias_value(const ElementType* const array,const size_t size)
{
    //assert argument alignment at runtime in debug builds
    assert(uintptr_t(array) % alignof(ElementType) == 0);

    static const size_t min_element_count = (sizeof(ReturnType) / sizeof(ElementType)) + (sizeof(ReturnType) % sizeof(ElementType) != 0 ? 1 : 0);

    static_assert(!std::tr1::is_pointer<ReturnType>::value ? !std::tr1::is_const<ReturnType>::value : true, "Target type can't be a const value type");
    static_assert(std::tr1::is_pod<ReturnType>::value, "Target type must be POD");
    static_assert(std::tr1::is_pod<ElementType>::value, "Array elements must be of POD type");

    //check for minimum element count in array
    if(size < min_element_count)
        throw std::invalid_argument("insufficient array size");

    ReturnType r;
    memcpy(&r,array,sizeof(ReturnType));
    return r;
}

更高效的方法可以使用内在函数(如SSE中的内容)进行显式未对齐读取,以提取基元。


示例:

struct sample_struct
{
    char c[4];
    int _aligner;
};

int test(void)
{
    const sample_struct constPOD    = {};
    sample_struct pod               = {};
    const char* str                 = "abcd";

    const int* constIntPtr  = alias_cast<const int*>(&constPOD);
    void* voidPtr           = alias_value<void*>(pod);
    int intValue            = alias_value<int>(str,strlen(str));

    return 0;
}

编辑:

  • 可以改进确保仅转换POD的断言。
  • 删除了多余的模板助手,现在仅使用tr1特征
  • 用于澄清和禁止const值(非指针)返回类型的静态断言
  • 调试版本的运行时断言
  • 为某些函数参数添加了const限定符
  • 使用memcpy的另一种打字功能
  • 重构
  • 小例子

答案 2 :(得分:4)

我认为在最基本的层面上,这是不可能的,并且违反严格的别名。你唯一能做到的就是欺骗编译器注意不到。

答案 3 :(得分:2)

  

我的问题是,可以肯定的是,该计划是否符合标准?

没有。使用您提供的别名,对齐可能不自然。你写的联盟只是移动别名的点。它可能看起来有效,但当CPU选项,ABI或编译器设置发生变化时,该程序可能会失败。

  

如果没有,怎么能修改这个是合法的?

创建自然的临时变量并将存储视为内存blob(移入和移出blob的临时代码),或使用代表所有类型的联合(请记住,此处一次有一个活动元素)。