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
массив и он НЕ будет неопределенным поведением? Вот несколько простых примеров:
нет
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? Все ли они? Меняется ли ответ на этот вопрос между C++11 и C++1z?
1 ответ
Здесь действуют два правила:
[basic.lval] / 8, иначе, строгое правило псевдонимов: проще говоря, вы не можете получить доступ к объекту через указатель / ссылку на неправильный тип.
[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]). Но цель правил не изменилась.