我正在尝试构建一个类模板,它在一个适当大的char数组中包含一堆类型,并允许以单独的正确类型引用访问数据。现在,根据标准,这会导致严格别名违规,从而导致未定义的行为,因为我们通过与其不兼容的对象访问char[]
数据。具体而言,标准规定:
如果程序试图通过以下类型之一以外的glvalue访问对象的存储值,则行为未定义:
- 对象的动态类型,
- 对象的动态类型的cv限定版本,
- 与对象的动态类型相似的类型(如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::pair
或{{ 1}}。std::tuple
模板也应扩展为处理cv限定类型,只有满足对齐要求才能安全使用,但我希望这段代码足以证明这个想法。)
这个技巧通过g ++(使用alias_cast
编译时)使警告静音,并且代码可以工作,但这是否真的是告诉编译器跳过基于严格别名的优化的有效方法?
如果它无效,那么如何在不违反别名规则的情况下实现基于char数组的通用存储类?
编辑:
用这样的简单g++ -std=c++11 -Wall -Wextra -O2 -fstrict-aliasing -Wstrict-aliasing
替换alias_cast
:
reinterpret_cast
使用g ++编译时,会产生以下警告:
aliastest-so-1.cpp:实例化'T&amp; Data :: first()[with T = int; U = short unsigned int]':aliastest-so-1.cpp:28:16:
从这里需要aliastest-so-1.cpp:21:58:警告:解除引用 类型惩罚指针将破坏严格别名规则 [-Wstrict混叠
答案 0 :(得分:3)
如果你想坚持严格的一致性,使用联合几乎不是一个好主意,它们在阅读活跃成员时只有严格的规则(仅限这一个)。虽然必须说实现类似于使用联合作为可靠行为的钩子,也许这就是你所追求的。如果是这种情况,我会推荐迈克·阿克顿(Mike Acton)撰写了关于别名规则的a nice (and long) article,他在那里对通过联盟进行评论做了评论。
据我所知,这是你应该如何处理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
是一个,好吧,无关紧要 type)。
您仍然可以使用例如storage[0]
读取对象的字节,因为这是通过unsigned char
类型的glvalue读取对象值,这是列出的显式异常之一。另一方面,如果storage
具有不同但仍然微不足道的元素类型,您仍然可以使上述代码段工作,但无法执行storage[0]
。
使片段合理的最后一块是指针转换。请注意,reinterpret_cast
不适用于一般情况。它是有效的,因为T
是标准布局(对齐也有其他限制),但如果是这种情况,那么使用reinterpret_cast
将等同于static_cast
通过像我一样void
。首先直接使用该表单更有意义,特别是考虑到存储的使用在通用上下文中发生了很多。在任何情况下,转换为void
和从static_cast
转换是标准转换之一(具有明确定义的含义),并且您希望T* p = ::new (&storage) T;
用于那些转换。
如果您对指针转换(这是我认为最薄弱的环节,而不是关于存储重用的争论)感到担忧,那么另一种方法就是
std::aligned_storage
如果你想跟踪它,它会在存储中花费额外的指针。
我衷心建议使用{{1}}。