С++: приведение беззнакового символа к структуре

Что я пытаюсь сделать

typedef struct {
    unsigned char a;
    unsigned char b;
    unsigned int  c;
} Packet;

unsigned char buffer[] = {1, 1, 0, 0, 0, 1};
Packet pkt = (Packet)buffer;

В основном я пытаюсь преобразовать массив байтов в структуру на С++, при компиляции получаю:

No matching function call for Packet::Packet(unsigned char[6])

Это невозможно или мне нужно вручную индексировать массив?

В общем, всякий раз, когда вы чувствуете необходимость выполнить приведение типов в стиле C в своей программе на C++, вы должны воспринимать это как признак того, что вы делаете что-то неправильно.

Some programmer dude 20.01.2023 06:28

Что касается вашей проблемы, если размер структуры точно равен размеру массива, используйте массив в качестве указателя на объект структуры «Пакет» и скопируйте его в объект Packet.

Some programmer dude 20.01.2023 06:29

Не связанные, но важные: добавьте static_assert(sizeof(Packet) == 6);. Также обратите внимание, что в C++ вам не нужен typedef struct.

Evg 20.01.2023 06:31

Я бы просто вручную проиндексировал массив, это самый безопасный способ десериализации двоичных данных, поскольку он вообще предполагает, как структура размещается в памяти. Если у вас действительно есть проблемы с производительностью (или объемом памяти), начните оптимизацию. И этот метод приведет к UB, если вы попытаетесь преобразовать память в объект C++ (он не будет в допустимом состоянии, поскольку конструктор объектов не будет вызван)

Pepijn Kramer 20.01.2023 06:41
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
4
65
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Вы можете сделать это с помощью reinterpret_cast из массива:

Packet pkt = *reinterpret_cast<Packet*>(buffer);

Это превращает массив в указатель на его 1-й элемент, затем обрабатывает этот указатель как указатель Packet*, затем мы разыменовываем его и копируем в новую структуру Packet. Это обходит практически все проверки типа компилятора и безопасности, поэтому здесь нужно быть очень осторожным.

Одна вещь, которую мы можем сделать, чтобы сделать это немного безопаснее, — это использовать static_assert, чтобы убедиться, что структура соответствует размеру, который мы ожидаем. Затем это не удастся скомпилировать, если компилятор вставит какое-либо дополнение в определение структуры.

static_assert(sizeof(Packet) == 6);

В зависимости от вашего компилятора и настроек компиляции почти наверняка ваша структура в том виде, в каком она написана, НЕ 6 байт.

Каждый раз, когда вы используете reinterpret_cast, вы работаете очень близко к сфере неопределенного/зависимого от компилятора поведения. Вообще говоря, пока вы выполняете проверки заполнения и имеете дело с примитивными типами данных внутри структуры, все будет работать так, как вы ожидаете, даже если код технически не определен в соответствии со стандартом C++. Разработчики компиляторов понимают, что этот тип кода часто необходим, и поэтому обычно поддерживают его разумным образом, даже если это не требуется стандартом C++.

Ответ принят как подходящий

Есть несколько способов сделать это:

// packet.h
////////////////
struct Packet {
    unsigned char a;
    unsigned char b;
    unsigned int  c;
};

Если вы скомпилируете и выгрузите структуры с помощью pahole, вы увидите отступы

$ pahole -dr --structs main.o
struct Packet {
        unsigned char              a;                    /*     0     1 */
        unsigned char              b;                    /*     1     1 */

        /* XXX 2 bytes hole, try to pack */

        unsigned int               c;                    /*     4     4 */

        /* size: 8, cachelines: 1, members: 3 */
        /* sum members: 6, holes: 1, sum holes: 2 */
        /* last cacheline: 8 bytes */
};

Таким образом, это в основном 2 символа, 2 байта заполнения и 4 байта int, всего 8 байтов.

Поскольку Intel является платформой с прямым порядком байтов, младший байт идет первым, как в

void print_packet( Packet* pkt ) {
    printf( "a:%d b:%d c:%d\n", int(a), int(b), c );
}
int main() {
    unsigned char buffer[] = {1, 1, 0, 0, 1, 0, 0, 0};
    print_packet( (Packet*) buffer );
    print_packet( reinterpret_cast<Packet*>(buffer));
}

Производит:

$ g++ main.cpp -o main
$ ./main
a:1 b:1 c:1
a:1 b:1 c:1

Однако можно изменить упаковку из командной строки, как показано ниже, где мы устанавливаем выравнивание на 2 байта.

$ g++ -ggdb  main.cpp -o main -fpack-struct=2
$ pahole -dr --structs main
struct Packet {
        unsigned char              a;                    /*     0     1 */
        unsigned char              b;                    /*     1     1 */
        unsigned int               c;                    /*     2     4 */

        /* size: 6, cachelines: 1, members: 3 */
        /* last cacheline: 6 bytes */
} __attribute__((__packed__));

Тогда вы можете видеть, что структура Packet составляет всего 6 байт, а результат запуска main совершенно другой.

$ ./main
a:1 b:1 c:65536
a:1 b:1 c:65536

Это потому, что значение c теперь равно 0x00000100 или 65536

Поэтому, чтобы не быть во власти этих махинаций компилятора, лучше определить ваш пакет в коде с правильной упаковкой как

// packet.h
////////////////
struct [[gnu::packed]] Packet {
    unsigned char a;
    unsigned char b;
    unsigned char reserved[2];
    unsigned int  c;
};

Тогда исполнение становится

$ g++ -ggdb  main.cpp x.cpp -o main -fpack-struct=2
$ ./main
a:1 b:1 c:1
a:1 b:1 c:1
$ g++ -ggdb  main.cpp x.cpp -o main -fpack-struct=4
$ ./main
a:1 b:1 c:1
a:1 b:1 c:1
$ g++ -ggdb  main.cpp x.cpp -o main -fpack-struct=8
$ ./main
a:1 b:1 c:1
a:1 b:1 c:1
$ g++ -ggdb  main.cpp x.cpp -o main -fpack-struct=16
$ ./main
a:1 b:1 c:1
a:1 b:1 c:1

существуют специальные определения типов с гарантированными размерами. Вместо unsigned char следует использовать uint8_t или std::byte, начиная с C++17.

Sergey Kolesnik 20.01.2023 07:19

@SergeyKolesnik Даже если я заменю unsigned char на std::byte, заполняющая дыра в 2 байта все равно будет существовать.

Henrique Bucher 20.01.2023 07:29

А потом вы переключаете компилятор или настройки компилятора, или на другой компьютер (например, с 32-битной машины на 64-битную), и все снова ломается. Для поддерживаемого/переносимого кода это не сработает.

Pepijn Kramer 20.01.2023 07:47

@SergeyKolesnik Все еще не безопасно

Pepijn Kramer 20.01.2023 07:47

@PepijnKramer Я не обращался к каламбурам типов, поэтому ничего не сказал о безопасности кода. Код без UB будет включать std::copy массив байтов в созданную структуру POD (в любом случае, до C++ 20), без доступа к данным через reinterprec_cast/type каламбур

Sergey Kolesnik 20.01.2023 07:54

Прежде всего, ваше предположение о том, что байтовое представление вашей структуры точно такое же, как вы пишете в структуре, неверно для большинства современных архитектур. Например, в 32-битной архитектуре ваше определение будет эквивалентно

struct Packet {
  char a;
  char b;
  char __hidden_padding[2];
  int c;
};

Аналогичная вещь, но с другим количеством отступов будет происходить на 64-битной архитектуре. Итак, чтобы избежать этого, вам нужно сказать компилятору «упаковать» структуру без байтов заполнения. Для этого не существует стандартного синтаксиса, но большинство компиляторов предоставляют средства для этого. Например, для gcc/clang вы можете сделать:

struct [[gnu::packed]] Packet {
  char a;
  char b;
  int c;
};

Внимание, при работе с такими структурами не рекомендуется брать адреса их членов, см. Небезопасен ли пакет __attribute__((packed)) / #pragma gcc?.

Теперь, поскольку «простые» типы, такие как char, int и т. д., имеют размер, определяемый реализацией, гораздо лучше использовать типы фиксированного размера и, наконец, проверить, соответствует ли размер структуры ожидаемому, как предложил Evg:

struct [[gnu::packed]] Packet {
  int8_t a;
  int8_t b;
  int32_t c;
};
static_assert(sizeof(Packet) == 6);

Копирование лучше всего выполнять либо std::bit_cast, если у вас есть C++20, либо просто memcpy. Насколько я знаю, эти 2 являются только стандартными способами на сегодняшний день. Использование *reinterpret_cast<Packet*>(buffer) не определено, хотя по-прежнему работает для большинства компиляторов.

Другие вопросы по теме