注意:此问题已重命名并缩小,以使其更具针对性和可读性。大多数评论都参考了旧文本。
根据标准,不同类型的对象可能不共享相同的内存位置。所以这不合法:
std::array<short, 4> shorts;
int* i = reinterpret_cast<int*>(shorts.data()); // Not OK
但是,标准允许对此规则进行例外处理:可以通过指向char
或unsigned char
的指针访问任何对象:
int i = 0;
char * c = reinterpret_cast<char*>(&i); // OK
然而,我不清楚这是否也允许反过来。例如:
char * c = read_socket(...);
unsigned * u = reinterpret_cast<unsigned*>(c); // huh?
答案 0 :(得分:23)
由于涉及指针转换,您的一些代码值得怀疑。请记住,在这些实例中reinterpret_cast<T*>(e)
具有static_cast<T*>(static_cast<void*>(e))
的语义,因为涉及的类型是标准布局。 (事实上,我建议您在处理存储时总是通过static_cast
使用cv void*
。)
仔细阅读标准表明,在指针转换为T*
或从T*
转换时,假设确实存在实际对象magic_cast<T*>(p)
- 这在某些情况下难以实现片段,即使在作弊时也是如此。感谢所涉及的各种类型(稍后将详细介绍)。除此之外,那是因为......
别名与指针转换无关。这是C ++ 11文本,概述了通常称为“严格别名”的规则。规则,从3.10 Lvalues和rvalues [basic.lval]:
10如果程序试图通过以下类型之一以外的glvalue访问对象的存储值,则行为未定义:
- 对象的动态类型,
- 对象的动态类型的cv限定版本,
- 与对象的动态类型相似的类型(如4.4中所定义)
- 与对象的动态类型对应的有符号或无符号类型的类型
- 与对象的动态类型的cv限定版本对应的有符号或无符号类型的类型,
- 聚合或联合类型,包括其元素或非静态数据成员中的上述类型之一(递归地,包括子聚合或包含联合的元素或非静态数据成员),
- 一种类型,它是对象动态类型的(可能是cv限定的)基类类型,
- char或unsigned char类型。
(这是C ++ 03中同一条款和子条款的第15段,文本中有一些细微的变化,例如使用&#39; lvalue&#39;而不是&#39; glvalue&#39;后者是C ++ 11的概念。)
根据这些规则,让我们假设一个实现为我们提供了reinterpret_cast
,其中包含某些&#39;将指针转换为另一种指针类型。通常这个将为reinterpret_cast
,在某些情况下会产生未指定的结果,但正如我之前所解释的那样,对于指向标准布局类型的指针并非如此。然后,显然所有的片段都是正确的(用magic_cast
代替magic_cast
),因为magic_cast
的结果不会涉及任何glvalues。
以下是出现以错误地使用// 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;
为了证明我的推理,假设这个表面上不同的片段:
new (&c) int;
此代码段经过精心构建。特别是,在&c
中我允许使用c
,即使&c
由于3.8对象生命周期[basic.life]第5段中规定的规则而被销毁。它的第6段提供了与存储引用非常相似的规则,第7段解释了一旦存储被重用后用于引用对象的变量,指针和引用会发生什么 - 我将统称为3.8 / 5- 7。
在这种情况下,void*
(隐式)转换为p
,这是正确使用尚未重用的存储指针之一。同样,&c
是在构建新int
之前从c
获得的。它的定义也许可以在int
被破坏之后移动,取决于实施魔法的深度,但肯定不是在short
构造之后:第7段将适用,这不是允许的的情况。 p
对象的构造也依赖于int
成为存储的指针。
现在,因为short
和<new>
是微不足道的类型,我不必使用对析构函数的显式调用。我也不需要对构造函数进行显式调用(也就是说,调用q
中声明的通常的标准展示位置new)。从3.8对象生命期[basic.life]:
1 [...]类型为T的对象的生命周期始于:
- 获得具有类型T的适当对齐和尺寸的存储,并且
- 如果对象具有非平凡的初始化,则其初始化完成。
类型T的对象的生命周期在以下时间结束:
- 如果T是具有非平凡析构函数(12.4)的类类型,则析构函数调用开始,或
- 对象占用的存储空间被重用或释放。
这意味着我可以重写代码,以便在折叠中间变量p
之后,我最终得到原始代码段。
请注意alignas(alignment) char c[sizeof(int)];
*magic_cast<int*>(&c) = 42;
*magic_cast<short*>(&c) = 42;
不能折叠起来。也就是说,以下内容肯定是不正确的:
int
如果我们假设&c
对象(通常是)用第二行构造,则必须表示char c[sizeof(int)]
成为指向已重用的存储的指针。因此第三行是不正确的 - 尽管由于3.8 / 5-7并且严格来说不是由于混叠规则。
如果我们不这么认为,那么第二行 违反了别名规则:我们通过glvalue读取实际上是int
对象的内容类型*magic_cast<unsigned char>(&c) = 42;
,不是允许的例外之一。相比之下,short
会很好(我们假设在第三行上简单地构造了一个*some_magic_pointer = foo;
对象。)
就像Alf一样,我还建议您在使用存储时明确使用标准展示位置。跳过破坏琐碎的类型是很好的,但是当遇到char
时,您很可能面临违反3.8 / 5-7(无论获得指针多么神奇)或别名规则。这意味着也要存储新表达式的结果,因为一旦构造了对象,你很可能无法重用魔术指针 - 由于3.8 / 5-7再次出现。
读取对象的字节(这意味着使用unsigned char
或reinterpret_cast
)然而,你甚至不能使用static_cast
或任何魔法。通过cv void*
来{{1}}对于这项工作来说可以说是好的(虽然我觉得标准可以在那里使用更好的措辞)。
答案 1 :(得分:2)
关于......的有效性。
alignas(int) char c[sizeof(int)];
int * i = reinterpret_cast<int*>(c);
reinterpret_cast
本身是否正常,从产生有用的指针值的角度来看,具体取决于编译器。并且在此示例中未使用结果,特别是不访问字符数组。所以关于这个例子的说法并没有多少:它只是取决于。
但是让我们考虑一个触及别名规则的扩展版本:
void foo( char* );
alignas(int) char c[sizeof( int )];
foo( c );
int* p = reinterpret_cast<int*>( c );
cout << *p << endl;
让我们只考虑编译器保证一个有用的指针值的情况,一个将指针放在相同的内存字节中(这取决于编译器的原因是标准,在§5.2.10/ 7,仅保证指针转换,其中类型是对齐兼容的,否则将其保留为“未指定”(但是,整个§5.2.10与§9.2/ 18有些不一致) )。
现在,对标准§3.10/ 10的一种解释,即所谓的“严格别名”条款(但请注意标准不会使用术语“严格别名”),
如果程序试图通过以下类型之一以外的glvalue访问对象的存储值,则行为未定义:
- 对象的动态类型,
- 对象的动态类型的cv限定版本,
- 与对象的动态类型相似的类型(如4.4中所定义)
- 与对象的动态类型对应的有符号或无符号类型的类型
- 与对象的动态类型的cv限定版本对应的有符号或无符号类型的类型,
- 聚合或联合类型,包括其元素或非静态数据成员中的上述类型之一(递归地,包括子聚合或包含联合的元素或非静态数据成员),
- 一种类型,它是对象动态类型的(可能是cv限定的)基类类型,
char
或unsigned
char
类型。
就像它本身所说的那样,涉及驻留在c
字节中的对象的动态类型。
根据该解释,如果*p
已在其中放置foo
对象,则int
上的读取操作即可,否则无效。因此,在这种情况下,通过char
指针访问int*
数组。并且没有人怀疑另一种方式是否有效:即使foo
可能在这些字节中放置了int
个对象,您也可以最后一段§3.10/ 10,可以自由地将该对象作为一系列char
值进行访问。
通过这种(通常的)解释,在foo
放置int
后,我们可以将其作为char
个对象访问,因此至少存在一个char
对象在名为c
的内存区域内;我们可以int
访问它,因此至少存在一个int
;所以David’s assertion in another answer char
个对象无法作为int
访问,与此通常的解释不符。
David的断言也与最常用的贴身新用法不相容。
关于其他可能的解释,也许可以与大卫的断言相容,好吧,我想不出任何有意义的解释。
总而言之,就神圣标准而言,仅仅向自己投射一个T*
指向数组的指针实际上是有用的或不取决于编译器,并且访问指向的可能值是是否有效取决于存在的内容。特别是,想一想int
的陷阱表示:如果位模式恰好是那样的话,你不会想要炸毁你。所以为了安全起见,你必须知道那里有什么,比特,以及上面对foo
的调用说明,编译器通常不知道,就像g ++编译器的严格对齐一样基于优化器的优化器通常不知道...