reinterpret_cast,char*和未定义的行为

Bar*_*rry 13 c++ undefined-behavior language-lawyer reinterpret-cast c++17

什么情况下,reinterpret_cast荷兰国际集团一char*(或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
Run Code Online (Sandbox Code Playgroud)

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

  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?
    
    Run Code Online (Sandbox Code Playgroud)

    什么时候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?
    
    Run Code Online (Sandbox Code Playgroud)
  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;
    // ...
    }
    
    Run Code Online (Sandbox Code Playgroud)

UB中有任何一种情况吗?他们都是?这个问题的答案是否在C++ 11到C++ 1z之间发生了变化?

Nic*_*las 5

这里有两个规则:

  1. [basic.lval]/8,又名严格别名规则:简单地说,你不能通过指针/引用错误的类型访问对象.

  2. [base.life]/8:简单地说,如果你为不同类型的对象重用存储,你就不能使用指向旧对象的指针而不先对它们进行清洗.

这些规则是区分"存储位置"或"存储区域"和"对象"的重要部分.

您的所有代码示例都成为同一问题的牺牲品:它们不是您将它们投射到的对象:

alignas(int) char data[sizeof(int)];
Run Code Online (Sandbox Code Playgroud)

这会创建一个类型的对象char[sizeof(int)].该对象不是一个int.因此,您可能无法像访问它一样访问它.无论是读还是写都没关系; 你仍然挑衅UB.

同理:

char* data_ptr = new char[sizeof(int)];
Run Code Online (Sandbox Code Playgroud)

这也创建了一个类型的对象char[sizeof(int)].

char buffer[100];
Run Code Online (Sandbox Code Playgroud)

这会创建一个类型的对象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;
// ...
}
Run Code Online (Sandbox Code Playgroud)

请注意,我们正在谈论语言状态将是未定义的行为.很可能编译器对这些中的任何一个都没问题.

这个问题的答案是否在C++ 11到C++ 1z之间发生了变化?

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

  • @Barry:放置 new 开始对象的生命周期,即使该存储中已经有一个对象。第一条语句将“char[4]”放入该存储中。第二条语句结束“char[4]”的生命周期并开始“int”的生命周期。 (2认同)