由于涉及的指针转换,您的某些代码是有问题的。请记住,在这些情况下reinterpret_cast<T*>(e)
具有语义,static_cast<T*>(static_cast<void*>(e))
因为所涉及的类型是标准布局。(事实上,我建议您在处理存储时始终使用static_cast
via 。)cv void*
仔细阅读标准表明,在指针转换到或从指针转换过程中,T*
假定确实T*
涉及到一个实际的对象——这在你的一些片段中很难实现,即使是“作弊”,这要归功于类型的琐碎性涉及(稍后会详细介绍)。但是,那将是题外话,因为...
别名与指针转换无关。这是 C++11 文本,概述了通常称为“严格别名”规则的规则,来自 3.10 Lvalues 和 rvalues [basic.lval]:
10 如果程序尝试通过非下列类型之一的左值访问对象的存储值,则行为未定义:
- 对象的动态类型,
- 对象的动态类型的 cv 限定版本,
- 与对象的动态类型类似(如 4.4 中定义)的类型,
- 与对象的动态类型相对应的有符号或无符号类型,
- 对应于对象动态类型的 cv 限定版本的有符号或无符号类型,
- 聚合或联合类型,在其元素或非静态数据成员中包括上述类型之一(递归地包括子聚合或包含联合的元素或非静态数据成员),
- 一个类型,它是对象的动态类型的(可能是 cv 限定的)基类类型,
- char 或 unsigned char 类型。
(这是 C++03 中相同子句的第 15 段,文本中有一些细微的变化,例如使用 'lvalue' 而不是 'glvalue',因为后者是 C++11 的概念。)
根据这些规则,让我们假设一个实现为我们提供了magic_cast<T*>(p)
“以某种方式”将指针转换为另一种指针类型的方法。通常这将是reinterpret_cast
,在某些情况下会产生未指定的结果,但正如我之前解释的那样,对于指向标准布局类型的指针而言,情况并非如此。那么很明显,您的所有片段都是正确的(用 替换reinterpret_cast
)magic_cast
,因为magic_cast
.
这是一个似乎错误使用的片段magic_cast
,但我认为它是正确的:
// assume constexpr max
constexpr auto alignment = max(alignof(int), alignof(short));
alignas(alignment) char c[sizeof(int)];
// I'm assuming here that the OP really meant to use &c and not c
// this is, however, inconsequential
auto p = magic_cast<int*>(&c);
*p = 42;
*magic_cast<short*>(p) = 42;
为了证明我的推理,假设这个表面上不同的片段:
// alignment same as before
alignas(alignment) char c[sizeof(int)];
auto p = magic_cast<int*>(&c);
// end lifetime of c
c.~decltype(c)();
// reuse storage to construct new int object
new (&c) int;
*p = 42;
auto q = magic_cast<short*>(p);
// end lifetime of int object
p->~decltype(0)();
// reuse storage again
new (p) short;
*q = 42;
这个片段是精心构建的。特别是,即使由于 3.8 对象生命周期 [basic.life] 的第 5 段中规定的规则而被销毁,new (&c) int;
我仍允许使用。相同的第 6 段给出了与存储引用非常相似的规则,第 7 段解释了一旦对象的存储被重用,用于引用对象的变量、指针和引用会发生什么——我将它们统称为 3.8/5- 7.&c
c
在这种情况下&c
(隐式)转换为void*
,这是对尚未重用的存储的指针的正确使用之一。类似地p
是从构造&c
新的之前获得的。int
它的定义可能会移到 销毁之后c
,这取决于实现魔法的深度,但肯定不是在int
构造之后:第 7 段将适用,这不是允许的情况之一。对象的构造short
也依赖于p
成为指向存储的指针。
现在,因为int
和short
是微不足道的类型,我不必使用对析构函数的显式调用。我也不需要对构造函数的显式调用(也就是说,对通常在中声明的标准放置 new 的调用<new>
)。从 3.8 对象生命周期 [basic.life] 开始:
1 [...] T 类型对象的生命周期开始于:
- 获得具有适合类型 T 的对齐和大小的存储,并且
- 如果对象有非平凡的初始化,它的初始化就完成了。
T 类型对象的生命周期在以下情况下结束:
- 如果 T 是具有非平凡析构函数(12.4)的类类型,则析构函数调用开始,或者
- 对象占用的存储空间被重用或释放。
这意味着我可以重写代码,以便在折叠中间变量之后q
得到原始代码段。
请注意p
不能折叠。也就是说,下面的内容肯定是不正确的:
alignas(alignment) char c[sizeof(int)];
*magic_cast<int*>(&c) = 42;
*magic_cast<short*>(&c) = 42;
如果我们假设一个int
对象是(平凡地)用第二行构造的,那么这一定意味着&c
成为一个指向已被重用的存储的指针。因此第三行是不正确的——尽管严格来说是由于 3.8/5-7 而不是由于别名规则。
如果我们不假设,那么第二行违反char c[sizeof(int)]
了别名规则:我们正在通过类型的 glvalue读取实际上是一个对象int
,这不是允许的例外之一。相比之下,*magic_cast<unsigned char>(&c) = 42;
就可以了(我们假设short
在第三行简单地构造了一个对象)。
就像 Alf 一样,我还建议您在使用存储时明确使用标准放置 new。跳过琐碎类型的破坏很好,但是当遇到时,*some_magic_pointer = foo;
您很可能面临违反 3.8/5-7(无论获得该指针的方式多么神奇)或别名规则。这也意味着存储新表达式的结果,因为一旦构造了对象,您很可能无法重用魔术指针——再次由于 3.8/5-7。
但是,读取对象的字节(这意味着使用char
or unsigned char
)很好,而且您甚至根本不需要使用reinterpret_cast
或任何魔术。static_cast
viacv void*
可以说很适合这项工作(尽管我确实觉得标准可以在那里使用更好的措辞)。