Reinterpret_cast, char * и undefined

Каковы случаи, когда reinterpret_cast ing a char* (или char[N]) - это поведение undefined, и когда это определено поведение? Каково правило, которое я должен использовать, чтобы ответить на этот вопрос?


Как мы узнали из этого вопроса, следующее поведение undefined:

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 и не иметь это поведение undefined? Вот несколько простых примеров:

  • Нет 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?

  • Что делать, если 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?
    
  • Что делать, если я просто получаю структуры на проводе и хочу условно отбросить их в зависимости от того, что такое первый байт?

    // 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? Все ли они? Изменяется ли ответ на этот вопрос между С++ 11 и С++ 1z?

Ответ 1

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

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

  • [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]. Этот объект не является ни a MsgType1, ни a 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;
// ...
}

Обратите внимание, что мы говорим о том, какие состояния языка будут undefined. Коэффициенты хороши, что компилятор был бы в порядке с любым из них.

Изменяется ли ответ на этот вопрос между С++ 11 и С++ 1z?

Были некоторые важные разъяснения правил с С++ 11 (особенно [basic.life]). Но намерение, лежащее в основе правил, не изменилось.