Reinterpret_cast, char*和未定义行为

reinterpret_cast, char*, and undefined behavior

本文关键字:未定义 char cast Reinterpret      更新时间:2023-10-16

在什么情况下,reinterpret_castchar*(或char[N])是未定义的行为,何时是已定义的行为?我应该用什么经验法则来回答这个问题?


从这个问题中我们得知,以下是未定义的行为:

alignas(int) char data[sizeof(int)];
int *myInt = new (data) int;           // OK
*myInt = 34;                           // OK
int i = *reinterpret_cast<int*>(data); // <== UB! have to use std::launder

但是在什么时候我们可以在char数组上做reinterpret_cast并使其不是未定义的行为?下面是一些简单的例子:

  1. 没有new,只有reinterpret_cast:

    alignas(int) char data[sizeof(int)];
    *reinterpret_cast<int*>(data) = 42;    // is the first cast write UB?
    int i = *reinterpret_cast<int*>(data); // how about a read?
    *reinterpret_cast<int*>(data) = 4;     // how about the second write?
    int j = *reinterpret_cast<int*>(data); // or the second read?
    

    int的生存期何时开始?是否附有data的申报单?如果是,data的生存期什么时候结束?

  2. 如果data是一个指针呢?

    char* data_ptr = new char[sizeof(int)];
    *reinterpret_cast<int*>(data_ptr) = 4;     // is this UB?
    int i = *reinterpret_cast<int*>(data_ptr); // how about the read?
    
  3. 如果我只是在网上接收结构体,并希望根据第一个字节有条件地转换它们,该怎么办?

    // bunch of handle functions that do stuff with the members of these types
    void handle(MsgType1 const& );
    void handle(MsgTypeF const& );
    char buffer[100]; 
    ::recv(some_socket, buffer, 100)
    switch (buffer[0]) {
    case '1':
        handle(*reinterpret_cast<MsgType1*>(buffer)); // is this UB?
        break;
    case 'F':
        handle(*reinterpret_cast<MsgTypeF*>(buffer));
        break;
    // ...
    }
    

这些情况中是否有UB?都是吗?从c++ 11到c++ 1z,这个问题的答案会改变吗?

这里有两个规则:

  1. [基本。Lval]/8,也就是严格混叠规则:简单地说,你不能通过指向错误类型的指针/引用来访问对象。

  2. [基地。Life]/8:简单地说,如果你为不同类型的对象重用存储空间,你不能在不清洗指针的情况下使用指向旧对象的指针。

这些规则是区分"内存位置"的重要部分。或"存储区域";和"一个对象"

你所有的代码示例都陷入了同样的问题:它们不是你强制转换的对象:

alignas(int) char data[sizeof(int)];

创建一个类型为char[sizeof(int)]的对象。该对象是而不是int。因此,您可能无法像访问它一样访问它。不管它是读还是写;你还是激怒了UB。

同样:

char* data_ptr = new char[sizeof(int)];

还创建了一个类型为char[sizeof(int)]的对象。

char buffer[100];

创建一个类型为char[100]的对象。该对象既不是MsgType1也不是MsgTypeF。所以你不能像访问它一样访问它。

请注意,这里的UB是当您作为Msg*类型之一访问缓冲区时,而不是当您检查第一个字节时。如果所有的Msg*类型都是可复制的,那么读取第一个字节,然后将缓冲区复制到适当类型的对象中是完全可以接受的。

switch (buffer[0]) {
case '1':
    {
        MsgType1 msg;
        memcpy(&msg, buffer, sizeof(MsgType1));
        handle(msg);
    }
    break;
case 'F':
    {
        MsgTypeF msg;
        memcpy(&msg, buffer, sizeof(MsgTypeF));
        handle(msg);
    }
    break;
// ...
}

请注意,我们讨论的是语言状态下的未定义行为。很有可能编译器对这些都很好。

这个问题的答案在c++ 11和c++ 1z之间有变化吗?

自c++ 11以来有一些重要的规则澄清(特别是[basic.life])。但是这些规则背后的意图并没有改变。

相关文章: