允许使用char *对T *进行别名化。是否也允许反过来?

时间:2012-09-27 00:45:24

标签: c++ language-lawyer strict-aliasing type-punning

注意:此问题已重命名并缩小,以使其更具针对性和可读性。大多数评论都参考了旧文本。


根据标准,不同类型的对象可能不共享相同的内存位置。所以这不合法:

std::array<short, 4> shorts;
int* i = reinterpret_cast<int*>(shorts.data()); // Not OK

但是,标准允许对此规则进行例外处理:可以通过指向charunsigned char的指针访问任何对象:

int i = 0;
char * c = reinterpret_cast<char*>(&i); // OK

然而,我不清楚这是否也允许反过来。例如:

char * c = read_socket(...);
unsigned * u = reinterpret_cast<unsigned*>(c); // huh?

2 个答案:

答案 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 charreinterpret_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限定的)基类类型,
  •   
  • charunsigned 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 ++编译器的严格对齐一样基于优化器的优化器通常不知道...