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

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


Как мы узнали из этого вопроса, следующее поведение 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 и НЕ иметь неопределенного поведения? Вот несколько простых примеров:

  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?


person Barry    schedule 10.09.2016    source источник
comment
(1) мне кажется действительным. В обоих операторах вызывается новый объект int и ему присваивается значение. Чтение этой ценности - вот где начинаются неприятности. То же самое с (2) (при условии sizeof(int)==4). (3) для меня выглядит как UB.   -  person Igor Tandetnik    schedule 10.09.2016
comment
@IgorTandetnik уточнил вопросы и немного почитал, и избавился от предположения о sizeof(int), спасибо.   -  person Barry    schedule 10.09.2016
comment
Теперь (1) и (2), похоже, демонстрируют UB по тем же причинам, что и связанный вопрос. Было бы легко спасти указатель от первого приведения и использовать его для всех последующих операций записи и чтения.   -  person Igor Tandetnik    schedule 10.09.2016
comment
Кажется, что большинство компиляторов ведут себя так, как вы от них ожидаете, даже если это точно не определено. Дополнительную информацию см. Здесь: stackoverflow.com/questions/39381726/   -  person user2296177    schedule 11.09.2016
comment
@ user2296177: Нерелевантно для вопроса с тегом language-lawyer. ; -]   -  person ildjarn    schedule 11.09.2016
comment
С P0137 [intro.object] / 1 делает его кристально понятным при создании объектов. Ни в одном из первых двух примеров нет живого int объекта в data или data_ptr.   -  person T.C.    schedule 12.09.2016


Ответы (1)


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

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

  2. [base.life] / 8: проще говоря, если вы повторно используете хранилище для объекта другого типа, вы не можете использовать указатели на старый объект (ы), не отмыв их предварительно.

Эти правила являются важной частью различения между «ячейкой памяти» или «областью хранения» и «объектом».

Все ваши примеры кода становятся жертвой одной и той же проблемы: это не тот объект, к которому вы их применяете:

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

Это создает объект типа char[sizeof(int)]. Этот объект не int. Следовательно, вы не можете получить к нему доступ, как если бы он был. Не имеет значения, чтение это или запись; Вы еще УБ провоцируете.

Сходным образом:

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

person Nicol Bolas    schedule 11.09.2016
comment
Разве объявление моего массива char не потенциально означает получение хранилища для некоторого еще не инициализированного пустым образом типа T? В этом смысле, разве здоровое разбрызгивание launder не сделает все четко определенным? - person Barry; 11.09.2016
comment
@Barry: std::launder не для этого. Если вы начнете время жизни объекта в хранилище более старого, это позволит вам получить указатель на новый объект из указателя на старый. Это не запускает жизнь чего-либо. Не объявляет ли мой массив char потенциально не получение хранилища для некоего еще не инициализированного пустым образом типа T? По этой логике любой объект мог бы быть пока- предполагаемый пусто-инициализированный тип T. В конце концов, у объекта есть хранилище. char[X] - такой же объект, как и любой другой объект. - person Nicol Bolas; 11.09.2016
comment
Но именно тогда [basic.life] говорит, что время жизни объекта начинается - когда память приобретается. Учитывая char buf[4]; int* i = new (buf) int;, когда начинается время жизни int, на которое указывает i? - person Barry; 11.09.2016
comment
@Barry: новое размещение начинает время жизни объекта, даже если объект уже был в этом хранилище. Первый оператор помещает char[4] в это хранилище. Второй оператор завершает время жизни char[4] и начинает время жизни int. - person Nicol Bolas; 11.09.2016
comment
Получает ли место новое место для хранения? Хранилище уже есть. - person Barry; 11.09.2016
comment
@Barry: Есть ли место для нового размещения? Да. Новый синтаксис размещения просто предоставляет дополнительные аргументы функциям распределения. В этом случае аргумент void* вызывает вызов перегрузки operator new, которая просто возвращает переданный параметр. Но это все еще функция распределения, и она все еще получает память. Ничего не было сказано о новом хранилище. - person Nicol Bolas; 11.09.2016