首页 文章

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

提问于
浏览
11

我'm trying to build a class template that packs a bunch of types in a suitably large char array, and allows access to the data as individual correctly typed references. Now, according to the standard this can lead to strict-aliasing violation, and hence undefined behavior, as we'通过与其不兼容的对象访问 char[] 数据 . 具体来说,标准规定:

如果程序试图通过以下类型之一以外的glvalue访问对象的存储值,则行为未定义:对象的动态类型,对象的动态类型的cv限定版本,a类型(如4.4中所定义)类型为对象的动态类型,类型是对应的动态类型的有符号或无符号类型,类型是对应于cv限定版本的有符号或无符号类型对象的动态类型,聚合或联合类型,包括其元素或非静态数据成员中的上述类型之一(递归地,包括子聚合或包含联合的元素或非静态数据成员),一种类型,它是对象的动态类型(可能是cv限定的)基类类型,char或unsigned 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;
}

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

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

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

编辑:用简单的 reinterpret_cast 替换 alias_cast ,如下所示:

T& first() { return reinterpret_cast<T&>(*(data_ + 0)); }
U& second() { return reinterpret_cast<U&>(*(data_ + sizeof(T))); }

使用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走样]

1 回答

  • 3

    如果你想坚持严格的一致性,使用联合几乎不是一个好主意,它们在阅读活跃成员时只有严格的规则(仅限这一个) . 虽然必须说实现类似于使用联合作为可靠行为的钩子,也许这就是你所追求的 . 如果是这种情况,我会按照别名规则编写a nice (and long) article的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));
    

    定义为有效的原因是 T 是此处对象的动态类型 . 当新表达式创建了 T 对象时,存储被重用,该操作隐式地结束了 storage 的生命周期(这通常是因为 unsigned char 是一个很好的平凡类型) .

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

    使片段合理的最后一块是指针转换 . 请注意, reinterpret_cast 不适用于一般情况 . 它可以是有效的,因为 T 是标准布局(对齐也有额外的限制),但如果是这种情况,那么使用 reinterpret_cast 将等同于 static_cast 通过 void 就像我做的那样 . 首先直接使用该表单更有意义,特别是考虑到存储的使用在通用上下文中发生了很多 . 在任何情况下,转换为 void 和从 void 转换是标准转换之一(具有明确定义的含义),并且您希望 static_cast 用于那些转换 .

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

    T* p = ::new (&storage) T;
    

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

    我衷心建议使用 std::aligned_storage .

相关问题