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

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

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

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 без вызова неопределенного поведения или выполнения ненужной копии?

4b9b3361

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

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