基于通用char []的存储并避免与严格别名相关的UB

mit*_*ull 11 c++ strict-aliasing undefined-behavior type-punning c++11

我正在尝试构建一个类模板,它在一个适当大的char数组中包含一堆类型,并允许作为单独的正确类型引用访问数据.现在,根据标准,这会导致严格别名违规,从而导致未定义的行为,因为我们char[]通过与其不兼容的对象访问数据.具体来说,标准规定:

如果程序试图通过以下类型之一以外的glvalue访问对象的存储值,则行为未定义:

  • 对象的动态类型,
  • 一个cv限定版本的动态类型的对象,
  • 与对象的动态类型类似的类型(如4.4中所定义),
  • 与对象的动态类型对应的有符号或无符号类型的类型,
  • 一种类型,是有符号或无符号类型,对应于对象动态类型的cv限定版本,
  • 聚合或联合类型,包括其元素或非静态数据成员中的上述类型之一(递归地,包括子聚合或包含联合的元素或非静态数据成员),
  • 一个类型,它是对象动态类型的(可能是cv限定的)基类类型,
  • a charunsigned char类型.

鉴于突出显示的要点的措辞,我提出了以下alias_cast想法:

#include <iostream>
#include <type_traits>

template <typename T>
T alias_cast(void *p) {
    typedef typename std::remove_reference<T>::type BaseType;
    union UT {
        BaseType t;
    };
    return reinterpret_cast<UT*>(p)->t;
}

template <typename T, typename U>
class Data {
    union {
        long align_;
        char data_[sizeof(T) + sizeof(U)];
    };
public:
    Data(T t = T(), U u = U()) { first() = t; second() = u; }
    T& first() { return alias_cast<T&>(data_); }
    U& second() { return alias_cast<U&>(data_ + sizeof(T)); }
};


int main() {
    Data<int, unsigned short> test;
    test.first() = 0xdead;
    test.second() = 0xbeef;
    std::cout << test.first() << ", " << test.second() << "\n";
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

(上面的测试代码,特别是这个Data类只是一个愚蠢的想法,所以请不要指出我应该如何使用std::pairstd::tuple.alias_cast模板也应该扩展到处理cv限定类型,它只能是安全的如果符合对齐要求,则使用,但我希望这段代码足以证明这个想法.)

这个技巧通过g ++(编译时g++ -std=c++11 -Wall -Wextra -O2 -fstrict-aliasing -Wstrict-aliasing)使警告静音,并且代码有效,但这是否真的是告诉编译器跳过基于严格别名的优化的有效方法?

如果它无效,那么如何在不违反别名规则的情况下实现基于char数组的通用存储类?

编辑:用这样alias_cast的简单替换reinterpret_cast:

T& first() { return reinterpret_cast<T&>(*(data_ + 0)); }
U& second() { return reinterpret_cast<U&>(*(data_ + sizeof(T))); }
Run Code Online (Sandbox Code Playgroud)

使用g ++编译时会产生以下警告:

aliastest-so-1.cpp:实例化'T&Data :: first()[用T = int; U = short unsigned int]':aliastest-so-1.cpp:28:16:
从这里需要aliastest-so-1.cpp:21:58:警告:解除引用类型惩罚指针将破坏严格别名规则[ - Wstrict走样]

Luc*_*ton 3

如果您想坚持严格的一致性,那么使用联合几乎从来都不是一个好主意,他们在读取活跃成员(仅此一个)时有严格的规则。尽管不得不说,实现喜欢使用联合作为可靠行为的钩子,也许这就是您所追求的。如果是这种情况,我会听取 Mike Acton 的意见,他写了一篇关于别名规则的好(而且很长)的文章,其中他对通过联合进行转换发表了评论。

据我所知,这是您应该如何处理 char 类型数组作为存储的方式:

// char or unsigned char are both acceptable
alignas(alignof(T)) unsigned char storage[sizeof(T)];
::new (&storage) T;
T* p = static_cast<T*>(static_cast<void*>(&storage));
Run Code Online (Sandbox Code Playgroud)

定义它起作用的原因是T 这里对象的动态类型。当 new 表达式创建对象时,存储被重用T,该操作隐式地结束了对象的生命周期storage(这很简单unsigned char,因为这是一个简单的类型)。

您仍然可以使用 egstorage[0]读取对象的字节,因为这是通过unsigned char类型的泛左值读取对象值,这是列出的显式异常之一。另一方面,如果storage属于不同但仍然微不足道的元素类型,您仍然可以使上面的代码片段工作,但无法执行storage[0].

使代码片段变得有意义的最后一部分是指针转换。请注意,这reinterpret_cast不适用于一般情况。它可以是有效的,因为它T是标准布局(对齐也有额外的限制),但如果是这种情况,那么 usingreinterpret_cast将相当于static_casting viavoid。首先直接使用该形式更有意义,特别是考虑到在通用上下文中经常使用存储。无论如何,转换为和转换为void标准转换之一(具有明确定义的含义),并且您需要static_cast这些转换。

如果您完全担心指针转换(我认为这是最薄弱的环节,而不是关于存储重用的争论),那么另一种选择是

T* p = ::new (&storage) T;
Run Code Online (Sandbox Code Playgroud)

如果你想跟踪它,这会在存储中花费一个额外的指针。

我衷心推荐使用std::aligned_storage.