Как интерпретировать полезную нагрузку сообщения без нарушения правил псевдонимов типов?

Моя программа получает сообщения по сети. Эти сообщения десериализуются некоторым промежуточным программным обеспечением (т.е. другим кодом, который я не могу изменить). Моя программа получает объекты, которые выглядят примерно так:

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 Создание неявного объекта для манипуляций с объектом низкого уровня.

На данный момент, в моем собственном коде, я не разбираюсь с дефектами в стандарте, я предпочитаю следить за поведением компилятора, потому что для этого предмета это кодер и компилятор, которые делают правила, и стандарт будет просто следовать!