reinterpret_cast, char* и неопределенное поведение

Каковы случаи, когда 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

Но в какой момент мы можем сделать 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?
    

    Когда срок службы для 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 ответ

Решение

Здесь действуют два правила:

  1. [basic.lval] / 8, иначе, строгое правило псевдонимов: проще говоря, вы не можете получить доступ к объекту через указатель / ссылку на неправильный тип.

  2. [base.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]). Но цель правил не изменилась.

Другие вопросы по тегам