C ++ массивы переменной длины в структуре

Я пишу программу для создания, отправки, получения и интерпретации пакетов ARP. У меня есть структура, представляющая заголовок ARP, как это:

struct ArpHeader
{
unsigned short hardwareType;
unsigned short protocolType;
unsigned char hardwareAddressLength;
unsigned char protocolAddressLength;
unsigned short operationCode;
unsigned char senderHardwareAddress[6];
unsigned char senderProtocolAddress[4];
unsigned char targetHardwareAddress[6];
unsigned char targetProtocolAddress[4];
};

Это работает только для аппаратных адресов с длиной 6 и протокольных адресов с длиной 4. Длина адреса также указана в заголовке, поэтому для правильной структуры структура должна выглядеть примерно так:

struct ArpHeader
{
unsigned short hardwareType;
unsigned short protocolType;
unsigned char hardwareAddressLength;
unsigned char protocolAddressLength;
unsigned short operationCode;
unsigned char senderHardwareAddress[hardwareAddressLength];
unsigned char senderProtocolAddress[protocolAddressLength];
unsigned char targetHardwareAddress[hardwareAddressLength];
unsigned char targetProtocolAddress[protocolAddressLength];
};

Это, очевидно, не будет работать, так как длина адреса не известна во время компиляции. Шаблонные структуры также не подходят, так как я хотел бы заполнить значения для структуры, а затем просто привести ее из (ArpHeader *) к (char *), чтобы получить байтовый массив, который можно отправить по сети или преобразовать. полученный массив байтов от (char *) до (ArpHeader *) для его интерпретации.

Одним из решений было бы создание класса со всеми полями заголовка в качестве переменных-членов, функция для создания байтового массива, представляющего заголовок ARP, который может быть отправлен в сети, и конструктор, который будет принимать только байтовый массив (полученный в сети) и интерпретировать его, читая все поля заголовка и записывая их в переменные-члены. Это не очень хорошее решение, так как для этого потребуется намного больше кода.

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

#pragma pack(push, 1)
#pragma pack(pop)

вокруг объявления структуры, чтобы я мог на самом деле выполнить простое приведение в стиле C, чтобы получить байтовый массив для отправки по сети.

Есть ли какое-нибудь решение, которое я мог бы использовать здесь, которое было бы близко к структуре или, по крайней мере, не требовало бы намного больше кода, чем структура?
Я знаю, что последнее поле в структуре (если это массив) не требует определенного размера во время компиляции, могу ли я использовать что-то подобное для моей проблемы? Просто оставив размеры этих 4 массивов пустыми, мы скомпилируем, но я понятия не имею, как это на самом деле будет работать. Проще говоря, это не может работать, так как компилятор не будет знать, где начинается второй массив, если размер первого массива неизвестен.

5

Решение

Вам нужен достаточно низкоуровневый пакет ARP, и вы пытаетесь найти способ правильно определить структуру данных, чтобы можно было вложить этот объект в эту структуру. Вместо этого вы можете использовать интерфейс поверх блоба.

struct ArpHeader {
mutable std::vector<uint8_t> buf_;

template <typename T>
struct ref {
uint8_t * const p_;
ref (uint8_t *p) : p_(p) {}
operator T () const { T t; memcpy(&t, p_, sizeof(t)); return t; }
T operator = (T t) const { memcpy(p_, &t, sizeof(t)); return t; }
};

template <typename T>
ref<T> get (size_t offset) const {
if (offset + sizeof(T) > buf_.size()) throw SOMETHING;
return ref<T>(&buf_[0] + offset);
}

ref<uint16_t> hwType() const { return get<uint16_t>(0); }
ref<uint16_t> protType () const { return get<uint16_t>(2); }
ref<uint8_t> hwAddrLen () const { return get<uint8_t>(4); }
ref<uint8_t> protAddrLen () const { return get<uint8_t>(5); }
ref<uint16_t> opCode () const { return get<uint16_t>(6); }

uint8_t *senderHwAddr () const { return &buf_[0] + 8; }
uint8_t *senderProtAddr () const { return senderHwAddr() + hwAddrLen(); }
uint8_t *targetHwAddr () const { return senderProtAddr() + protAddrLen(); }
uint8_t *targetProtAddr () const { return targetHwAddr() + hwAddrLen(); }
};

Если тебе надо const Правильность, вы удаляете mutable, создать const_refи дублировать методы доступа вconst версии, и сделать const Версии возвращаются const_ref а также const uint8_t *,

9

Другие решения

Короткий ответ: вы просто не можете иметь типы переменного размера в C ++.

Каждый тип в C ++ должен иметь известный (и стабильный) размер во время компиляции. Оператор IE sizeof() должен дать последовательный ответ. Обратите внимание, что вы можете иметь типы, которые содержат переменное количество данных (например: std::vector<int>), используя кучу, но размер фактического объекта всегда постоянен.

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

Обычно проблема решается путем написания (или генерации) функций-членов, которые анализируют входные данные и инициализируют данные объекта. Это в основном старая проблема сериализации данных, которая решалась бесчисленное количество раз за последние 30 лет.

Вот макет базового решения:

class packet {
public:
// simple things
uint16_t hardware_type() const;

// variable-sized things
size_t sender_address_len() const;
bool copy_sender_address_out(char *dest, size_t dest_size) const;

// initialization
bool parse_in(const char *src, size_t len);

private:
uint16_t hardware_type_;
std::vector<char> sender_address_;
};

Заметки:

  • код выше показывает очень простую структуру, которая позволит вам сделать следующее:

    packet p;
    if (!p.parse_in(input, sz))
    return false;
    
  • современный способ сделать то же самое через RAII будет выглядеть так:

    if (!packet::validate(input, sz))
    return false;
    
    packet p = packet::parse_in(input, sz);  // static function
    // returns an instance or throws
    
3

Если вы хотите сохранить простой доступ к данным и самим данным publicЕсть способ достичь того, что вы хотите, без изменения способа доступа к данным. Во-первых, вы можете использовать std::string вместо массивов char для хранения адресов:

#include <string>
using namespace std; // using this to shorten notation. Preferably put 'std::'
// everywhere you need it instead.
struct ArpHeader
{
unsigned char hardwareAddressLength;
unsigned char protocolAddressLength;

string senderHardwareAddress;
string senderProtocolAddress;
string targetHardwareAddress;
string targetProtocolAddress;
};

Затем вы можете перегрузить оператор преобразования operator const char*() и конструктор arpHeader(const char*) (и конечно operator=(const char*) желательно тоже), чтобы сохранить текущие функции отправки / получения, если это то, что вам нужно.

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

operator const char*(){
char* myRepresentation;
unsigned char mySize
= 2+ senderHardwareAddress.length()
+ senderProtocolAddress.length()
+ targetHardwareAddress.length()
+ targetProtocolAddress.length();

// We need to store the size, since it varies
myRepresentation = new char[mySize+1];
myRepresentation[0] = mySize;
myRepresentation[1] = hardwareAddressLength;
myRepresentation[2] = protocolAddressLength;

unsigned int offset = 3; // just to shorten notation
memcpy(myRepresentation+offset, senderHardwareAddress.c_str(), senderHardwareAddress.size());
offset += senderHardwareAddress.size();
memcpy(myRepresentation+offset, senderProtocolAddress.c_str(), senderProtocolAddress.size());
offset += senderProtocolAddress.size();
memcpy(myRepresentation+offset, targetHardwareAddress.c_str(), targetHardwareAddress.size());
offset += targetHardwareAddress.size();
memcpy(myRepresentation+offset, targetProtocolAddress.c_str(), targetProtocolAddress.size());

return myRepresentation;
}

Хотя конструктор можно определить так:

ArpHeader& operator=(const char* buffer){

hardwareAddressLength = buffer[1];
protocolAddressLength = buffer[2];

unsigned int offset = 3; // just to shorten notation
senderHardwareAddress = string(buffer+offset, hardwareAddressLength);
offset += hardwareAddressLength;
senderProtocolAddress = string(buffer+offset, protocolAddressLength);
offset += protocolAddressLength;
targetHardwareAddress = string(buffer+offset, hardwareAddressLength);
offset += hardwareAddressLength;
targetProtocolAddress = string(buffer+offset, protocolAddressLength);

return *this;
}
ArpHeader(const char* buffer){
*this = buffer; // Re-using the operator=
}

Тогда использовать ваш класс так же просто, как:

ArpHeader h1, h2;
h1.hardwareAddressLength = 3;
h1.protocolAddressLength = 10;
h1.senderHardwareAddress = "foo";
h1.senderProtocolAddress = "something1";
h1.targetHardwareAddress = "bar";
h1.targetProtocolAddress = "something2";

cout << h1.senderHardwareAddress << ", " << h1.senderProtocolAddress
<< " => " << h1.targetHardwareAddress << ", " << h1.targetProtocolAddress << endl;

const char* gottaSendThisSomewhere = h1;
h2 = gottaSendThisSomewhere;

cout << h2.senderHardwareAddress << ", " << h2.senderProtocolAddress
<< " => " << h2.targetHardwareAddress << ", " << h2.targetProtocolAddress << endl;

delete[] gottaSendThisSomewhere;

Который должен предложить вам необходимую утилиту и поддерживать ваш код в рабочем состоянии, ничего не изменяя из класса.

Однако обратите внимание, что если вы хотите немного изменить оставшуюся часть кода (здесь речь идет о том, что вы уже написали, за исключением класса), jxhответ должен работать так же быстро, как и это внутренняя сторона.

2