reinterpret_cast vs严格别名

for*_*818 8 c++ strict-aliasing language-lawyer reinterpret-cast

我正在阅读有关严格别名的内容,但它仍然有点模糊,我无法确定定义/未定义行为的界限.我发现最详细的帖子集中在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';
}
Run Code Online (Sandbox Code Playgroud)

请不要过多关注可能的越界访问以及可能没有必要写这样的事实.我知道char*允许指向任何东西,但我也有一个T*指向a char*.也许还有一些我想念的东西.

这是一个完整的例子,包括push/pop via memcpy,afaik不受严格别名的影响.

TL; DR:上面的代码是否表现出未定义的行为(暂时忽略了一个超出范围的访问),如果是,为什么?C++ 11或其中一个较新的标准有什么变化吗?

小智 7

我知道char*允许指向任何东西,但我也有一个T*指向a char*.

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

与C不同,C++不允许即兴创建对象*.您不能简单地将某个内存位置指定为类型T并创建该类型的对象,您需要具有该类型的对象.这需要放置new.以前的标准很不明确,但目前,每[intro.object]:

1 [...] 对象由定义(6.1),new-expression(8.3.4)创建,隐式更改union的活动成员(12.3)或创建临时对象时(7.4) ,15.2).[...]

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

此外,C++不会隐含地考虑指向同一地址的不同对象的指针.您&data[pos]计算指向char对象的指针.将其强制转换为T*不会指向T驻留在该地址的任何对象,并且取消引用该指针具有未定义的行为.C++ 17补充说std::launder,这是一种让编译器知道你想要访问该地址的不同对象的方法,而不是指向你的指针.

当您修改代码以使用放置new并且std::launder确保没有未对齐的访问时(我假设您为了简洁而将其留下),您的代码将具有已定义的行为.

*有关在未来版本的C++中允许这样做的讨论.


Swi*_*Pie 5

别名是两个引用相同对象的情况。那可能是引用或指针。

int x;
int* p = &x;
int& r = x;
// aliases: x, r ? *p  refer to same object.
Run Code Online (Sandbox Code Playgroud)

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

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;"
}
Run Code Online (Sandbox Code Playgroud)

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

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

int main() {
    int a = 0;

    std::cout << a << std::endl; 
    x = foo(reinterpret_cast<float*>(&a), &a);
    std::cout << a << "\n";   // Surprise? 
}
Run Code Online (Sandbox Code Playgroud)

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

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

严格的别名适用于工会成员,这是单独描述的,但原因是相同的:写入工会的一个成员并不能保证其他成员的值会改变。这不适用于存储在union中的结构开头的共享字段。因此,禁止通过联合进行类型修剪。(出于历史原因和维护遗留代码的便利,大多数编译器不赞成这样做。)

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

8如果程序尝试通过以下类型之一以外的glvalue访问对象的存储值,则行为未定义

(8.1)—对象的动态类型,

(8.2)—对象的动态类型的cv限定版本,

(8.3)—与对象的动态类型类似的类型(定义见7.5),

(8.4)—一种类型,是与对象的动态类型相对应的有符号或无符号类型,

(8.5)—一种类型,是与对象的动态类型的CV限定版本相对应的有符号或无符号类型,

(8.6)—集合或联合类型,在其元素或非静态数据成员(包括递归地包括子集合或所包含的联合的元素或非静态数据成员)中包括上述类型之一,

(8.7)—一种类型,它是对象的动态类型的(可能是经过cv限定的)基类类型,

(8.8)— char,unsigned char或std :: byte类型。

在7.5中

1类型T的cv分解是cvi和Pi的序列,使得对于n> 0,T为“ cv0 P0 cv1 P1····cvn?1 Pn?1 cvn U”,其中每个cvi是一组cv -限定符(6.9.3),并且每个Pi是“指向”的指针(11.3.1),“指向类型为Ci的类的成员的指针”(11.3.3),“ Ni的数组”或“未知范围的数组” (11.3.4)。如果Pi指定一个数组,则元素类型上的cv限定词cvi + 1也被视为该数组的cv限定词cvi。[示例:用type-id const int **表示的类型具有两个cv分解,将U用作“ int”和“指向const int的指针”。— —结束示例] T的最长cv分解中的第一个,即cv1,cv2,...之后的cv限定词的n元组。。。cvn称为T的cv资格签名。

2如果两个类型T1和T2具有n相同的cv分解,从而对应的Pi分量相同并且用U表示的类型相同,则它们是相似的。

结果是:虽然您可以将指针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
int* pc2 = reinterpret_cast<char*>(pi+2)); 
char c = *pc2; // no problem, unless increment didn't put us beyond array bound.
Run Code Online (Sandbox Code Playgroud)

重新解释演员表也不会创建它们指向的对象,并且为不存在的对象分配值是UB,因此,如果它指向的类不是很简单的话,则不能使用演员表的取消引用结果来存储数据。