Что я пытаюсь сделать
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])
Это невозможно или мне нужно вручную индексировать массив?
Что касается вашей проблемы, если размер структуры точно равен размеру массива, используйте массив в качестве указателя на объект структуры «Пакет» и скопируйте его в объект Packet.
Не связанные, но важные: добавьте static_assert(sizeof(Packet) == 6);. Также обратите внимание, что в C++ вам не нужен typedef struct.
Я бы просто вручную проиндексировал массив, это самый безопасный способ десериализации двоичных данных, поскольку он вообще предполагает, как структура размещается в памяти. Если у вас действительно есть проблемы с производительностью (или объемом памяти), начните оптимизацию. И этот метод приведет к UB, если вы попытаетесь преобразовать память в объект C++ (он не будет в допустимом состоянии, поскольку конструктор объектов не будет вызван)





Вы можете сделать это с помощью 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.
@SergeyKolesnik Даже если я заменю unsigned char на std::byte, заполняющая дыра в 2 байта все равно будет существовать.
А потом вы переключаете компилятор или настройки компилятора, или на другой компьютер (например, с 32-битной машины на 64-битную), и все снова ломается. Для поддерживаемого/переносимого кода это не сработает.
@SergeyKolesnik Все еще не безопасно
@PepijnKramer Я не обращался к каламбурам типов, поэтому ничего не сказал о безопасности кода. Код без UB будет включать std::copy массив байтов в созданную структуру POD (в любом случае, до C++ 20), без доступа к данным через reinterprec_cast/type каламбур
Прежде всего, ваше предположение о том, что байтовое представление вашей структуры точно такое же, как вы пишете в структуре, неверно для большинства современных архитектур. Например, в 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) не определено, хотя по-прежнему работает для большинства компиляторов.
В общем, всякий раз, когда вы чувствуете необходимость выполнить приведение типов в стиле C в своей программе на C++, вы должны воспринимать это как признак того, что вы делаете что-то неправильно.