C ++的严格别名规则 - 'char'别名豁免是双向的吗?

时间:2016-05-16 17:38:27

标签: c++ strict-aliasing

几周前,我了解到C ++标准有一个严格的别名规则。基本上,我问了一个关于移位的问题 - 而不是一次一个地移动每个字节,以最大化性能我想加载我的处理器的本机寄存器(分别为32或64位)并执行4/8的移位所有字节都在一条指令中。

这是我想避免的代码:

unsigned char buffer[] = { 0xab, 0xcd, 0xef, 0x46 };

for (int i = 0; i < 3; ++i)
{
  buffer[i] <<= 4; 
  buffer[i] |= (buffer[i + 1] >> 4);
}
buffer[3] <<= 4;

相反,我想使用类似的东西:

unsigned char buffer[] = { 0xab, 0xcd, 0xef, 0x46 };
unsigned int *p = (unsigned int*)buffer; // unsigned int is 32 bit on my platform
*p <<= 4;

有人在评论中提到我提出的解决方案违反了C ++别名规则(因为p的类型为int*,而缓冲区的类型为char*,我正在取消引用p来执行转换。请忽略对齐和字节顺序的可能问题 - 我处理这个片段以外的内容)我很惊讶地了解他严格别名规则,因为我经常对缓冲区中的数据进行操作,将其从一种类型转换为另一种类型并且从未有过任何问题。进一步的调查显示我使用的编译器(MSVC)没有强制执行严格的别名规则,因为我只是在闲暇时间使用gcc / g ++作为业余爱好,我可能还没有遇到过这个问题。 / p>

然后我问了一个关于严格别名规则和C ++的Placement new运算符的问题:

IsoCpp.org提供有关新展示位置的常见问题解答,并提供以下代码示例:

#include <new>        // Must #include this to use "placement new"
#include "Fred.h"     // Declaration of class Fred
void someCode()
{
  char memory[sizeof(Fred)];     // Line #1
  void* place = memory;          // Line #2
  Fred* f = new(place) Fred();   // Line #3 (see "DANGER" below)
  // The pointers f and place will be equal
  // ...
}

这个例子很简单,但我问自己,“如果有人在f上调用某个方法会怎么样 - 例如f->talk()?那时我们会取消引用f ,它指向与memory相同的内存位置(char*类型。我已经阅读了很多地方,对于char*类型的变量有一个免除任何类型的别名,但我感觉它不是“双向街道” - 意思是char*可以别名(读/写)任何类型T,但类型T只能是如果char*本身属于T,我习惯使用char*别名。当我输入此内容时,这对我没有任何意义,因此我倾向于相信我的初始(位移示例)违反严格别名规则的说法是假的。

有人可以解释一下是正确的吗?我一直在努力去理解什么是合法的,什么不合法(尽管已经阅读过很多关于这个主题的网站和SO帖子)

谢谢

2 个答案:

答案 0 :(得分:6)

别名规则意味着语言只承诺指针解除引用有效(即不触发未定义的行为),如果:

  • 通过兼容类的指针访问对象:正确的类或其超类之一,正确转换。这意味着如果B是D的超类,并且D* d指向有效的D,则访问static_cast<B*>(d)返回的指针是正常的,但访问reinterpret_cast<B*>(d)返回的指针是。后者可能未能解释D中B子对象的布局。
  • 您可以通过指向char的指针来访问它。由于char是字节大小和字节对齐的,因此在char*能够从D*读取数据时,您无法从reinterpret_cast<T*>读取数据。

也就是说,标准中的其他规则(特别是关于数组布局和POD类型的规则)可以被读取为确保您可以使用指针和char来别名 POD类型和int* ia = new int[3]; char* pc = reinterpret_cast<char*>(ia); // Possibly in some other function int* pi = reinterpret_cast<int*>(pc); 数组之间的双向如果确保具有适当大小的和对齐的char数组。

换句话说,这是合法的:

char* some_buffer; size_t offset; // Possibly passed in as an argument
int* pi = reinterpret_cast<int*>(some_buffer + offset);
pi[2] = -5;

虽然可能调用未定义的行为:

int

即使我们可以确保缓冲区大到足以包含三个char s,但对齐可能不正确。与所有未定义行为的实例一样,编译器可能会做任何事情。三种常见的事件可能是:

  • 代码可能是Just Work(TM),因为在您的平台中,所有内存分配的默认对齐方式与int相同。
  • 指针转换可能会将地址四舍五入到int的对齐方式(例如pi = pc&amp; -4),这可能会使您对错误的内存进行读/写操作。
  • 指针取消引用本身可能会以某种方式失败:CPU可能会拒绝未对齐的访问,从而导致应用程序崩溃。

由于你总是希望像魔鬼本身一样避开UB,你需要一个具有正确大小和对齐的new数组。获得它的最简单方法就是从“右”类型的数组开始(在本例中为int),然后通过char指针填充它,因为int是POD类型,所以这是允许的。

附录:使用展示位置new后,您将可以调用该对象上的任何函数。如果构造正确并且由于上述原因而未调用UB,那么您已在所需位置成功创建了一个对象,因此即使对象是非POD(例如因为它具有虚函数),任何调用都是正常的。毕竟,任何分配器类will likely use placement new都可以在它们获取的存储中创建对象。请注意,如果您使用展示位置.c3-chart-arc,这仅一定正确;类型惩罚的其他用法(例如,带有fread / fwrite的天真序列化)可能会导致对象不完整或不正确,因为对象中的某些值需要专门处理以维护类不变量。

答案 1 :(得分:0)

事实上,通过严格混叠对指针类型打孔的标准规则的解释不一定正确或易于理解。标准没有提及严格的别名,我发现原始的标准措辞更容易理解和推理。

本质上,它表示只能通过指向适合访问此对象的相关类型(例如相同类型或相关类类型)或通过指向char*的指针来访问对象

如您所见,双向街道的问题是&#39;甚至不适用。