C ++ variable length arrays in structure

I am writing a program for creating, sending, receiving and interpreting ARP packets. I have a structure representing an ARP header as follows:

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]; }; 

This only works for hardware addresses of length 6 and protocol addresses of length 4. The length of the address is also indicated in the header, so to be correct, the structure should look something like this:

 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]; }; 

This obviously will not work, since the lengths of the addresses are not known at compile time. Pattern structures are not an option, since I would like to fill in the values ​​for the structure, and then just drop it (ArpHeader *) to (char *) to get an array of bytes that can be sent on the network or discard the received byte array from (char *) before (ArpHeader *) to interpret it.

One solution would be to create a class with all the header fields as member variables, a function to create a byte array representing the ARP header that can be sent on the network, and a constructor that will only accept the byte array (received on the network) and interpret him by reading all the header fields and writing them to member variables. This is not a good solution, as it will require LOT more code.

Otherwise, a similar structure for the UDP header, for example, is simple, since all header fields have a known constant size. I use

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

around the structure declaration so that I can do a simple C-style to get an array of bytes to be sent on the network.

Is there any solution that I could use here that would be close to the structure, or at least not require much more code than the structure? I know that the last field in the structure (if it is an array) does not need a certain amount of compilation time, can I use something like this for my problem? Just leaving the dimensions of these 4 arrays empty will compile, but I have no idea how this will work. It just doesn't logically work, because the compiler had no idea where the second array starts if the size of the first array is unknown.

+5
source share
3 answers

You need a low-level thing, an ARP package, and you are trying to find a way to correctly define the data structure so that you can insert blob into this structure. Instead, you can use the interface above the blob.

 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(); } }; 

If you need const validity, you delete mutable , create const_ref and duplicate accessors in non- const versions, and return const versions const_ref and const uint8_t * .

+6
source

Short answer: you simply cannot have variable types in C ++.

Each type in C ++ must have a known (and stable) size at compile time. The IE operator sizeof() should give a consistent answer. Note: you can have types that contain a variable amount of data (for example: std::vector<int> ) using a bunch, but the size of the actual object is always constant.

That way, you can never create an ad of the type you would choose and get magical field settings. This fits deeply into the basic layout of the object - each member (aka field) must have a known (and stable) offset.

Typically, a problem is solved by writing (or generating) member functions that analyze input data and initialize object data. This is basically an old data serialization problem that has been solved countless times in the last 30 or so years.

Here is a basic solution layout:

 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_; }; 

Notes:

  • the above code shows the most basic structure that will allow you to do the following:

     packet p; if (!p.parse_in(input, sz)) return false; 
  • The modern way to do the same through RAII would look like this:

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

If you want to keep access to simple data and public data itself, there is a way to achieve what you want without changing the way you access data. First, you can use std::string instead of char arrays to store addresses:

 #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; }; 

Then you can overload the conversion operator const char*() , and the arpHeader(const char*) constructor (and, of course, operator=(const char*) preferred too) so that your current send / receive functions work if you need it .

A simplified conversion operator (skipping some fields to make it less complicated, but you shouldn't have any problems adding them) would look like this:

 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; } 

Although the constructor can be defined as such:

 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= } 

Then using your class is as simple as:

 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; 

Which should offer you a useful utility and save your code without changing anything from the class.

Note that if you want to change the rest of the code a bit (speaking here of what you already wrote, the ouside class), the jxh answer should work as fast as this, and more elegant on the inside.

+2
source

Source: https://habr.com/ru/post/1202987/


All Articles