reinterpret_cast与严格别名

时间:2018-08-23 09:38:48

标签: c++ language-lawyer strict-aliasing reinterpret-cast

我正在阅读严格的别名,但是它仍然有点模糊,而且我不确定所定义/未定义的行为在哪里。我发现最详细的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*指向任何内容,但是我也有一个T*指向char*。也许还有其他我想念的东西。

这里是complete example,还包括通过memcpy进行的推入/弹出,而afaik不受严格的别名影响。

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

3 个答案:

答案 0 :(得分:6)

  

我知道char*可以指向任何对象,但是我也有一个T*指向一个char*

对,那是一个问题。尽管指针强制转换本身已经定义了行为,但使用指针访问类型T的不存在的对象却不是。

与C不同,C ++不允许即兴创建对象 * 。您不能简单地将类型为T的内存分配给某个内存位置并创建该类型的对象,而您需要已经存在该类型的对象。这需要放置new。以前的标准对此尚不明确,但目前,每个[intro.object]均采用该标准:

  

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

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

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

当您修改代码以使用位置newstd::launder,并确保您没有错位的访问权限(为简洁起见,我假设您将其遗漏在外)时,您的代码将定义行为。

>

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

答案 1 :(得分:3)

别名是两种情况引用同一对象的情况。可能是引用或指针。

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; 
    x = foo(reinterpret_cast<float*>(&a), &a);
    std::cout << a << "\n";   // Surprise? 
}

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

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

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

自2017年标准版起: 6.10左值和右值

  

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

     

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

     

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

     

(8.3)-与动态类型相似的类型(如7.5中所定义)。   对象

     

(8.4)—一个类型,它是与以下类型对应的有符号或无符号类型   对象的动态类型,

     

(8.5)—一种类型,它是对应于a的有符号或无符号类型   cv限定的对象动态类型的版本,

     

(8.6)-包括以下类型之一的聚合或联合类型   元素或非静态数据成员中的上述类型   (包括递归地包括的元素或非静态数据成员   子集合或包含的联合)

     

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

     

(8.8)-字符,无符号字符或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-qualifiers cvi + 1也被视为   数组的cv限定词cvi。 [示例:由   type-id const int **具有两个cv分解,将U表示为“ int”,并且   作为“指向const int的指针”。 —结束示例] cv限定词的n元组   在T的最长cv分解中的第一个之后,即   cv1,cv2, 。 。 cvn称为T的cv资格签名。

     

2如果两个类型T1和T2的cv分解为   相同的n使得相应的Pi分量相同,并且   用U表示的类型相同。

结果是:尽管您可以重新解释_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.

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

答案 2 :(得分:2)

简短回答:

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

  2. 即使如此,您可能仍需要像C ++ 17和更高版本一样使用std::launder,因为您是通过指针{{1}访问创建的对象(类型T)的},类型为&data[pos]

仅在某些特殊情况下才允许使用“直接” char*,例如,当reinterpret_castTstd::bytechar时。

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