Как интерпретировать полезную нагрузку сообщения без нарушения правил псевдонимов типов?
Моя программа получает сообщения по сети. Эти сообщения десериализуются некоторым промежуточным программным обеспечением (т.е. другим кодом, который я не могу изменить). Моя программа получает объекты, которые выглядят примерно так:
struct Message {
int msg_type;
std::vector<uint8_t> payload;
};
Изучив msg_type
я могу определить, что полезная нагрузка сообщения - это, например, массив значений uint16_t
. Я хотел бы прочитать этот массив без ненужной копии.
Моя первая мысль заключалась в том, чтобы сделать это:
const uint16_t* a = reinterpret_cast<uint16_t*>(msg.payload.data());
Но тогда чтение из a
, похоже, нарушит стандарт. Вот пункт 3.10.10:
Если программа пытается получить доступ к сохраненному значению объекта с помощью glvalue, отличного от одного из следующих типов, поведение не определено:
- динамический тип объекта,
- cv-квалифицированная версия динамического типа объекта,
- тип, аналогичный (как определено в 4.4) для динамического типа объекта,
- тип, который является подписанным или неподписанным типом, соответствующим динамическому типу объекта,
- тип, который является подписанным или неподписанным типом, соответствующим версии с динамическим типом объекта cv,
- совокупный или тип объединения, который включает один из вышеупомянутых типов среди его элементов или нестатических членов данных (включая рекурсивно элемент или нестатический элемент данных субагрегата или содержащегося объединения),
- тип, который является (возможно, cv-квалифицированным) типом базового класса динамического типа объекта,
-
char
или unsigned char
type.
В этом случае a
будет glvalue, а uint16_t*
не соответствует ни одному из перечисленных критериев.
Итак, как мне обрабатывать полезную нагрузку как массив значений uint16_t
без вызова неопределенного поведения или выполнения ненужной копии?
Ответы
Ответ 1
Если вы собираетесь использовать значения один за другим, вы можете memcpy
на uint16_t
или написать payload[0] + 0x100 * payload[1]
и т.д., В uint16_t
, какое поведение вы хотите. Это не будет "неэффективно".
Если вам нужно вызвать функцию, которая принимает только массив uint16_t
, и вы не можете изменить структуру, которая передает Message
, тогда вам не повезло. В стандарте C++ вам придется сделать копию.
Если вы используете gcc или clang, другой параметр - установить -fno-strict-aliasing
при компиляции кода, о котором идет речь.
Ответ 2
Если вы хотите строго следовать C++ Standard без UB, а не использовать нестандартные расширения компилятора, вы можете попробовать:
uint16_t getMessageAt(const Message& msg, size_t i) {
uint16_t tmp;
memcpy(&tmp, msg.payload.data() + 2 * i, 2);
return tmp;
}
Оптимизация компилятора должна избегать копирования memcpy
здесь в сгенерированном машинный код; см., например, Type Punning, Strict Aliasing и Optimization.
Фактически, копирование в возвращаемое значение, но в зависимости от того, что вы будете с ним делать, эта копия также может быть оптимизирована (например, это значение может быть загружено в регистр и использоваться только там).
Ответ 3
Если вы хотите быть строго правильным, как указано в стандарте, который вы указали, вы не можете. Если вы хотите, чтобы поведение было четко определено, вам нужно будет сделать копию.
Если код предназначен для переносимости, вам нужно будет обрабатывать endianness в любом случае и восстановить ваши значения uint16_t из отдельных байтов uint8_t, и для этого по определению требуется копия.
Если вы действительно знаете, что делаете, вы можете игнорировать стандарт, и просто выполните описанный вами reinterpret_cast.
GCC и поддержка clang -fno-strict-aliasing
чтобы предотвратить оптимизацию генерации неработающего кода. Насколько мне известно, на момент написания этой статьи компилятор Visual Studio не имел флага и никогда не выполняет такие оптимизации - если вы не используете declspec(restrict)
или __restrict
.
Ответ 4
Ваш код может не быть UB (или пограничной линией в зависимости от чувствительности читателя), если, например, vector
данные были построены таким образом:
Message make_array_message(uint16_t* x, size_t n){
Message m;
m.type = types::uint16_t_array;
m.payload.reserve(sizeof(uint16_t)*n);
std::copy(x,x+n,reinterpret_cast<uint16_t*>(m.payload.data()));
return m;
}
В этом коде векторные данные содержат последовательность uint16_t
даже если она объявлена как uint8_t
. Таким образом, доступ к данным с помощью этого указателя:
const uint16_t* a = reinterpret_cast<uint16_t*>(msg.payload.data());
Отлично. Но доступ к vector
данным как uint8_t
был бы UB. Доступ a[1]
будет работать на всех компиляторах, но это UB в текущем стандарте. Это, возможно, дефект в стандарте, и комитет по стандартизации c++ работает над его исправлением, см. P0593 Создание неявного объекта для манипуляций с объектом низкого уровня.
На данный момент, в моем собственном коде, я не разбираюсь с дефектами в стандарте, я предпочитаю следить за поведением компилятора, потому что для этого предмета это кодер и компилятор, которые делают правила, и стандарт будет просто следовать!