C++ – How to store various sized values in a vector

c

I want to separate the communication protocol from the communication medium in a project. For example, I want to have a serial class and whatever other medium class and separately have a checksum protocol class and a crc protocol class and so forth. That way, a device class can have a member base class pointer to the medium and another to the protocol and via that device class's constructor child classes of both can be passed.

Pseudo code

class CDevice
{
public:
CDevice( CCommunicationDriver * drvr, CProtocol * protocol ) :
    drvr_     ( drvr ),
    protocol_ ( protocol )
{}

private:
CCommunicationDriver * drvr_;
CProtocol *            protocol_;
};

class CCrcProtcol : public CProtocol
{};

class CSerial : public CCommunicationDriver
{};

CDevice  a_device( new CSerial, new CCrcProtocol );

The idea is that if the difference between 2 devices is solely the medium and/or protocol a simple change can have a profound impact. The goal is minimal code rewrite for a change of this nature.

Ultimately, I envision the communication driver taking in a CPacket which is a thin wrapper around a std::vector. That way the interface for communicationdriver doesn't have to change going forward. I plan for the vector to hold unsigned char's ( bytes ) since ultimately everything going over a wire/medium is bytes.

This leads to commands that are decoupled from the medium and protocol albeit not completely decoupled. So, a command to turn on a light might have an ID 1 and a sub-ID 2. What I think I need to do is to pass child commands to the driver's transmit function [ tx( CCommand * ) ] and have that function apply the protocol's decoration ( not sure on how that'll work yet ) and then get a vector of bytes to have the child communication driver push out over the wire.

Thus, I have 2 questions I could use help with.
1. ( main question ) Since ultimately everything ends up in the std::vector< unsigned char > what's a flexible way to add data to that vector? I.e. a command has an id 2 bytes long due to one protocol but for another it's 4 bytes. How do I pass id and 2 or 4 to some function that will split it up into single bytes? Assume there's a lot sizes ( 1, 2, 3, 4, 8, etc. length ). I've thought about a template function that you specify the type with and then the value passed will be interpreted as that type's size.
template< typename T >
void addData( T data )
{
// determine data's type, parse somehow?
}

and also
template <typename T >
void addData( T data, int size )
{
// size is known but won't I have to specialize every size?
}
  1. Would you have the protocol class be a member of the driver class or keep them separate?

I hope this long explanation of what I'm trying to achieve is more helpful than annoying and that someone has a bright idea.

Thanks.
-G-

Best Answer

Firstly, command classes are decoupled from the medium and protocol. That means you can design the command classes for your own programming convenience, rather than having to design it to match exactly to the specifics of each protocol (which would be impossible, since different protocols may have different bit widths for the same command and field).

When I mention convenience, what I mean is that you can use the maximum bit width you'll ever need for each command's fields.

However, you may still need to have device- or protocol-specific validation code, since each device or protocol imposes its own limits to what values can be in those fields. Unless you don't plan to implement any validation at all.

When it comes to validation, there are several choices:

  • Not doing it at all, if you will be doing all of the programming yourself, and if it is a hobby project such that mistakes do not result in damages.
  • Validating it eagerly, i.e. in the command class. This may be difficult, since a command class might not know which device or protocol it will be sent to.
  • Validating it late, i.e. in the protocol class where the command values are being converted into bytes.

For example, even if a validation rule says that a particular field can only have a value in the range 0 - 100, it doesn't stop you from using a uint32_t or int32_t for that field in the command class.

To the second question of having an overloaded method that takes in various built-in number types and append the bytes to an internal byte vector, do notice the caveats.

In my opinion, if you only needs to work with the fundamental integer types, you don't need templates. Instead, you simply provide function overloads for each of the types, and you call the functions with a value of the appropriate type.

void CPacket::addData(uint32_t data) { ... }
void CPacket::addData(int32_t data) { ... }
void CPacket::addData(uint16_t data) { ... }
void CPacket::addData(int16_t data) { ... }
...

Regarding the code inside, there are several choices:

Type punning with union. This assumes that your code will work exclusively with one byte-endianness, thus not needing to consider the possibility of porting to a different byte-endianness.

union
{
    uint32_t value;
    uint8_t bytes[4];
} pun = { data };
// after that, add the bytes to the vector one-by-one, according to the byte endianness of the communication.

Explicitly extracting the bytes with endian-agnostic bitwise arithmetic: (see note on casting)

// only if value is unsigned. For signed value, it must first be cast to unsigned
uint8_t byte0 = (uint8_t)value;
uint8_t byte1 = (uint8_t)(value >> 8ul);
uint8_t byte2 = (uint8_t)(value >> 16ul);
uint8_t byte3 = (uint8_t)(value >> 24ul);