reinterpret_cast、char* 和未定义的行为

reinterpret_cast, char*, and undefined behavior

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


正如我们从 this question 中了解到的,以下是未定义的行为:

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. [basic.lval]/8,也就是严格的别名规则:简而言之,您不能通过 pointer/reference 访问错误类型的对象。

  2. [base.life]/8:简单地说,如果你为不同类型的对象重用存储,你不能在不洗钱的情况下使用指向旧对象的指针他们先。

这些规则是区分 "a memory location" 或 "a region of storage" 和 "an object" 的重要部分。

您的所有代码示例都遇到了同样的问题:它们不是您将它们转换为的对象:

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;
// ...
}

请注意,我们谈论的是语言声明的未定义行为。很有可能编译器可以处理其中任何一个。

Does the answer to this question change between C++11 to C++1z?

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