reinterpret_cast 与严格别名

reinterpret_cast vs strict aliasing

我正在阅读有关严格别名的内容,但它仍然有点模糊,我不确定定义/未定义行为的界限在哪里。我发现最详细的 post 集中在 C 上。所以如果你能告诉我这是否被允许以及自 C++98/11/...以来发生了什么变化,那就太好了...

#include <iostream>
#include <cstring>

template <typename T> T transform(T t);

struct my_buffer {
    char data[128];
    unsigned pos;
    my_buffer() : pos(0) {}
    void rewind() { pos = 0; }    
    template <typename T> void push_via_pointer_cast(const T& t) {
        *reinterpret_cast<T*>(&data[pos]) = transform(t);
        pos += sizeof(T);
    }
    template <typename T> void pop_via_pointer_cast(T& t) {
        t = transform( *reinterpret_cast<T*>(&data[pos]) );
        pos += sizeof(T);
    }            
};    
// actually do some real transformation here (and actually also needs an inverse)
// ie this restricts allowed types for T
template<> int transform<int>(int x) { return x; }
template<> double transform<double>(double x) { return x; }

int main() {
    my_buffer b;
    b.push_via_pointer_cast(1);
    b.push_via_pointer_cast(2.0);
    b.rewind();
    int x;
    double y;
    b.pop_via_pointer_cast(x);
    b.pop_via_pointer_cast(y);
    std::cout << x << " " << y << '\n';
}

请不要太在意可能的越界访问以及可能没有必要写那样的东西。我知道 char* 可以指向任何东西,但我还有一个指向 char*T*。也许还有其他我想念的东西。

这里有一个 complete example 也包括 push/pop 通过 memcpy,据我所知,它不受严格别名的影响。

TL;DR:上面的代码是否表现出未定义的行为(暂时忽略越界访问),如果是,为什么? C++11 或更新的标准之一有什么变化吗?

简答:

  1. 您不能这样做:*reinterpret_cast<T*>(&data[pos]) = 直到在指向的地址处构造了一个 T 类型的对象。您可以通过放置新的来完成。

  2. 即便如此,对于 C++17 及更高版本,您可能需要使用 std::launder,因为您通过指针访问创建的对象(T 类型) &data[pos] 类型 char*.

"Direct" reinterpret_cast 仅在某些特殊情况下才允许使用,例如,当 Tstd::bytecharunsigned char 时.

在 C++17 之前,我会使用基于 memcpy 的解决方案。编译器可能会优化掉任何不必要的副本。

I know that char* is allowed to point to anything, but I also have a T* that points to a char*.

对,这是个问题。虽然指针转换本身已经定义了行为,但使用它来访问类型为 T 的不存在的对象并不是。

与 C 不同,C++ 不允许即兴创建对象*。您不能简单地将某些内存位置分配为类型 T 并创建该类型的对象,您需要该类型的对象已经存在。这需要放置 new。以前的标准对此含糊不清,但目前,根据 [intro.object]:

1 [...] An object is created by a definition (6.1), by a new-expression (8.3.4), when implicitly changing the active member of a union (12.3), or when a temporary object is created (7.4, 15.2). [...]

由于您没有执行任何这些操作,因此不会创建任何对象。

此外,C++ 不会隐式地将指向同一地址的不同对象的指针视为等效指针。您的 &data[pos] 计算指向 char 对象的指针。将其转换为 T* 不会使其指向驻留在该地址的任何 T 对象,并且取消引用该指针具有未定义的行为。 C++17 添加了 std::launder,这是一种让编译器知道您想要访问该地址上的对象而不是指针指向的对象的方法。

当您修改您的代码以使用放置 newstd::launder,并确保您没有未对齐的访问(我想您为了简洁而忽略了它),您的代码将具有定义的行为。

* 正在讨论在未来的 C++ 版本中允许这样做。

别名是指两个实体引用同一对象的情况。它可以是引用或指针。

int x;
int* p = &x;
int& r = x;
// aliases: x, r и *p  refer to same object.

对于编译器来说,重要的是要期望如果一个值是使用一个名称编写的,那么它可以通过另一个名称访问。

int foo(int* a, int* b) {
  *a = 0;
  *b = 1;
  return *a; 
  // *a might be 0, might be 1, if b points at same object. 
  // Compiler can't short-circuit this to "return 0;"
}

现在如果指针是不相关的类型,编译器就没有理由期望它们指向相同的地址。这是最简单的UB:

int foo( float *f, int *i ) { 
    *i = 1;               
    *f = 0.f;            
   return *i;
}

int main() {
    int a = 0;

    std::cout << a << std::endl; 
    int x = foo(reinterpret_cast<float*>(&a), &a);
    std::cout << a << "\n"; 
    std::cout << x << "\n";   // Surprise?
}
// Output 0 0 0 or 0 0 1 , depending on optimization. 

简单地说,严格别名意味着编译器期望不相关类型的名称引用不同类型的对象,因此位于单独的存储单元中。因为用于访问这些存储单元的地址实际上是相同的,所以访问存储值的结果是不确定的,通常取决于优化标志。

memcpy() 通过获取地址,通过指向 char 的指针,并在库函数的代码中复制存储的数据来规避这种情况。

严格别名适用于union 成员,单独描述,但原因是相同的:写入union 的一个成员并不能保证其他成员的值发生变化。这不适用于存储在联合中的结构开头的共享字段。因此,禁止使用 union 进行类型双关。 (出于历史原因和维护遗留代码的方便,大多数编译器不遵守这一点。)

从 2017 年标准开始:6.10 左值和右值

8 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

(8.1) — the dynamic type of the object,

(8.2) — a cv-qualified version of the dynamic type of the object,

(8.3) — a type similar (as defined in 7.5) to the dynamic type of the object,

(8.4) — a type that is the signed or unsigned type corresponding to the dynamic type of the object,

(8.5) — a type that is the signed or unsigned type corresponding to a cv-qualified version of the dynamic type of the object,

(8.6) — an aggregate or union type that includes one of the aforementioned types among its elements or nonstatic data members (including, recursively, an element or non-static data member of a subaggregate or contained union),

(8.7) — a type that is a (possibly cv-qualified) base class type of the dynamic type of the object,

(8.8) — a char, unsigned char, or std::byte type.

在 7.5

1 A cv-decomposition of a type T is a sequence of cvi and Pi such that T is “cv0 P0 cv1 P1 · · · cvn−1 Pn−1 cvn U” for n > 0, where each cvi is a set of cv-qualifiers (6.9.3), and each Pi is “pointer to” (11.3.1), “pointer to member of class Ci of type” (11.3.3), “array of Ni”, or “array of unknown bound of” (11.3.4). If Pi designates an array, the cv-qualifiers cvi+1 on the element type are also taken as the cv-qualifiers cvi of the array. [ Example: The type denoted by the type-id const int ** has two cv-decompositions, taking U as “int” and as “pointer to const int”. —end example ] The n-tuple of cv-qualifiers after the first one in the longest cv-decomposition of T, that is, cv1, cv2, . . . , cvn, is called the cv-qualification signature of T.

2 Two types T1 and T2 are similar if they have cv-decompositions with the same n such that corresponding Pi components are the same and the types denoted by U are the same.

结果是:虽然您可以 reinterpret_cast 指向不同的、不相关且不相似的类型的指针,但您不能使用该指针访问存储的值:

char* pc = new char[100]{1,2,3,4,5,6,7,8,9,10}; // Note, initialized.
int* pi = reinterpret_cast<int*>(pc);  // no problem.
int i = *pi; // UB
char* pc2 = reinterpret_cast<char*>(pi+2);  // *(pi+2) would be UB
char c = *pc2; // no problem, unless increment didn't put us beyond array bound.
// c equals to 9

'reinterpret_cast' 不创建对象。在不存在的对象上取消引用指针是未定义的行为,因此如果 class 它指向的不是微不足道的,则不能使用强制转换的取消引用结果进行写入。