【问题标题】:C++'s Strict Aliasing Rule - Is the 'char' aliasing exemption a 2-way street?C++ 的严格别名规则 - 'char' 别名豁免是 2-way street 吗?
【发布时间】:2016-09-12 14:19:04
【问题描述】:

就在几周前,我了解到 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 以执行移位。(请忽略可能的问题对齐和字节顺序——我处理这个 sn-p 之外的那些)我很惊讶地了解到他的严格别名规则,因为我经常对缓冲区中的数据进行操作,将其从一种类型转换为另一种类型并且从未遇到任何问题. 进一步调查显示,我使用的编译器 (MSVC) 并没有强制执行严格的别名规则,而且由于我只是在业余时间作为爱好在 gcc/g++ 上进行开发,所以我可能还没有遇到这个问题。

然后我问了一个关于严格别名规则和 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-&gt;talk() 怎么办?那时我们将取消对f 的引用,它指向同一个内存位置为memorychar* 类型。我读过很多地方,char* 类型的变量可以豁免为任何类型的别名,但我的印象是它不是“两个-way street" -- 意思是,char* 可以别名(读/写)任何类型T,但类型T 只能用于别名char*,如果T 本身属于char*。在我输入此内容时,这对我没有任何意义,因此我倾向于相信我的初始(位移示例)违反严格别名规则的说法是错误的。

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

谢谢

【问题讨论】:

  • 如果调用f 的成员函数是未定义的行为,那会使放置新类型变得毫无用处,不是吗?
  • "它指向与内存相同的内存位置(类型为 char*)" - memory 的类型不是 char *。在第 2 行,数组衰减为一个指针,但这并不意味着 memory 是一个指针。而且,即使memory 是一个指针,它指向的内存位置的类型也是char,而不是char *
  • 看来你的问题是f-&gt;talk()是否可以;我认为删除所有序言(“那么”之前的东西)会改善这个问题

标签: c++ strict-aliasing


【解决方案1】:

事实上,通过严格别名来解释关于指针类型双关的标准规则不一定正确或容易理解。标准没有提到“严格别名”,我发现原始标准措辞更容易理解和推理。

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

如您所见,“双向街”的问题甚至不适用。

【讨论】:

  • 我不明白双向问题如何不适用。
  • "或者通过指向 char* 的指针" - 不,是通过指向 char 的指针。您已经引入了一定程度的间接性。
【解决方案2】:

别名规则意味着该语言仅在以下情况下承诺您的指针取消引用是有效的(即不会触发未定义的行为):

  • 您通过一个兼容类的指针访问一个对象:无论是它的实际类还是它的一个超类,正确地转换。这意味着如果 B 是 D 的超类并且您有 D* d 指向一个有效的 D,访问由 static_cast&lt;B*&gt;(d) 返回的指针是可以的,但访问由 reinterpret_cast&lt;B*&gt;(d) 返回的指针是 .后者可能未能说明 D 中 B 子对象的布局。
  • 您可以通过指向char 的指针访问它。由于 char 是字节大小和字节对齐的,因此您无法在从 char* 读取数据的同时从 D* 读取数据。

也就是说,标准中的其他规则(特别是关于数组布局和 POD 类型的规则)可以被解读为确保您可以使用指针和reinterpret_cast&lt;T*&gt; 来别名两个- POD 类型和char 数组之间的方式,如果你确保有一个适当大小的字符数组和对齐

换句话说,这是合法的:

int* ia = new int[3];
char* pc = reinterpret_cast<char*>(ia);
// Possibly in some other function
int* pi = reinterpret_cast<int*>(pc);

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

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

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

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

由于您总是想像魔鬼一样抵御 UB,因此您需要一个具有正确大小和对齐方式的 char 数组。最简单的方法是从一个“正确”类型(在本例中为 int)的数组开始,然后通过 char 指针填充它,这是允许的,因为 int 是 POD 类型。

附录: 使用放置new 后,您将能够调用对象上的任何函数。如果构造正确并且由于上述原因没有调用 UB,那么您已经在所需位置成功创建了一个对象,因此任何调用都可以,即使该对象是非 POD(例如,因为它具有虚函数)。毕竟,任何分配器类will likely use placement new 在他们获得的存储中创建对象。请注意,这仅在您使用放置new 时才必须正确;类型双关的其他用法(例如,使用 fread/fwrite 进行天真的序列化)可能会导致对象不完整或不正确,因为需要对对象中的某些值进行特殊处理以维护类不变量。

【讨论】:

  • [basic.stc.dynamic.allocation] 保证您的示例确实具有正确的对齐方式?
  • @Hurkyl new 不需要返回分配器返回的相同地址(即它可以返回一些偏移量)。所以,不,我不认为分配器要求保证new char[3*sizeof(int)] 一定会返回一个与int 对齐的指针,即使标准分配器保证这样做也是如此。
  • [expr.new]/11 保证正确对齐。尽管如此,它还是 UB,因为在任何地方都没有活着的 int 对象,只有一堆 chars。
  • @T.C. 如果 保证对齐,则reinterpret_cast POD 类型 不是 UB(我已经更新了我的答案)。由于字节顺序等原因,将获得的 是实现定义的,但不是未定义的。只有当您迁移到非 POD 类型时,它才会变成 UB,因为您可能会破坏类不变量、丢失 vtable 指针等。
  • 谢谢,reinterpret/static cast 的例子和解释很有见地。
猜你喜欢
  • 2014-07-13
  • 2015-03-30
  • 2015-10-15
  • 2021-12-22
  • 2013-03-11
  • 2019-01-29
  • 2015-05-31
  • 1970-01-01
相关资源
最近更新 更多