严格别名是否会阻止您通过不同类型写入 char 数组?

Does strict aliasing prevent you from writing to a char array through a different type?

我的理解是 C++ 中的严格别名定义在 basic.lval 11:

(11) If a program attempts to access the stored value of an object through a glvalue of other than one of the following types the behavior is undefined:

  • (11.1) the dynamic type of the object,
  • (11.2) a cv-qualified version of the dynamic type of the object,
  • (11.3) a type similar (as defined in conv.qual) to the dynamic type of the object,
  • (11.4) a type that is the signed or unsigned type corresponding to the dynamic type of the object,
  • (11.5) a type that is the signed or unsigned type corresponding to a cv-qualified version of the dynamic type of the object,
  • (11.6) an aggregate or union type that includes one of the aforementioned types among its elements or non-static data members (including, recursively, an element or non-static data member of a subaggregate or contained union),
  • (11.7) a type that is a (possibly cv-qualified) base class type of the dynamic type of the object,
  • (11.8) a char, unsigned char, or std​::​byte type.

根据我的阅读,根据 11.8,这始终是合法的,因为程序通过 unsigned char:

类型的左值访问 x 的存储值
int x = 0xdeadbeef;
auto y = reinterpret_cast<unsigned char*>(&x);
std::cout << y[1];

我很好奇使用别名指向 unsigned char:

数组的指针
alignas(int) unsigned char[4] x;
auto y = reinterpret_cast<int*>(x);
*y = 0xdeadbeef;

这是否违反了严格的别名?我的阅读是它不是,但是我刚刚在另一个线程上被告知它是。仅每个 basic.lval,在我看来没有 UB,因为程序不会尝试访问存储的值:它存储一个新值而不读取它,只要后续读取使用 x, 则不发生违规。

关于"access"的定义:

http://eel.is/c++draft/defns.access

3.1 access [defns.access]
⟨execution-time action⟩ read or modify the value of an object

换句话说,存储值也是"access"。还是UB

有许多调用 UB 的构造,但无论如何高质量的编译器都应该正确处理。使用字符类型存储来保存其他类型就是其中之一。要求 char[] 的构造函数产生指向对齐存储的指针,否则就没有意义。

C89 的作者认为没有必要完整描述适用于任何特定目的的高质量实现需要以可预测的方式运行的每种情况。基本原理认识到实现可能符合要求,但质量如此低以致于基本上无用,并表明没有必要禁止实现以损害其有用性的方式行事。每个后续的 C 或 C++ 标准都继承了 C89 中从未打算完全完成的部分,并且 none 已经完全完成了这些部分。

标准不区分

  • 调用 UB 的操作,但即使是最迟钝的编译器编写者也会认识到它们的行为应该是可预测的(例如 struct foo {int x;} s; s.x=1;);

  • 适合各种用途的优质编译器应该可预测地处理哪些操作,但只适合其他用途的低质量编译器或高质量编译器可能不会;

  • 某些编译器可以预见地处理的操作,但通常不应期望任何其他编译器进行此类处理——即使是那些针对相同目的(平台、应用领域等)的编译器。

声明一个具有特定对齐方式的 char[],使用命名数组一次来捕获其地址(并且不再使用命名数组),并将其用作可以容纳其他类型的原始存储,应该失败进入上面的第一类(特别是因为 - 如上所述 - 否则对齐保证不会有多大用处)。编译器可能无法识别任何指针与原始数组的关系,因此可能不会意识到对此类指针的操作可以与 char[](*) 交互,但如果数组永远不会再次用作 char[] 编译器没有理由关心。

(*) 例如给定

char foo[10];

int test(int *p)
{
  if (foo[1])
    *p = 1;
  return foo[1];
}

一个实现可能会缓存并重用从 foo[1] 读取的第一个值,而没有意识到写入 *p 可能会改变底层存储。但是,如果命名的左值 foo 在第一次获取其地址后从未使用过,那么编译器可能对缓存左值 foo 的读取是否安全做出什么样的假设并不重要, 因为 不会有任何 .