I'm building a code for an embedded system and I'm trying to save as much binary space as necessary.
The code is for parsing a protocol (MQTT for what it's worth), where there are numerous packets type and they are all different, but share some common parts.
Currently, to simplify writing the code, I'm using this pattern :
template <PacketType type>
struct ControlPacket
{
FixedHeader<type> type;
VariableHeader<type> header;
Properties<type> props;
... and so on...
};
// Specialize for each type
template <>
struct FixedHeader<CONNECT>
{
uint8_t typeAndFlags;
PacketType getType() const { return static_cast<PacketType>(typeAndFlags >> 4); }
uint8 getFlags() const { return 0; }
bool parseType(const uint8_t * buffer, int len)
{
if (len < 1) return false;
typeAndFlags = buffer[0];
return true;
}
...
};
template <>
struct FixedHeader<PUBLISH>
{
uint8_t typeAndFlags;
PacketType getType() const { return static_cast<PacketType>(typeAndFlags >> 4); }
uint8 getFlags() const { return typeAndFlags & 0xF; }
bool parseType(const uint8_t * buffer, int len)
{
if (len < 1) return false;
typeAndFlags = buffer[0];
if (typeAndFlags & 0x1) return false; // Example of per packet specific check to perform
return true;
}
...
};
... For all packet types ...
This is working, and I'm now trying to reduce the binary impact of all those template specializations (else the code is almost duplicated 16 times)
So, I've came up to this paradigm:
// Store the most common implementation in a base class
struct FixedHeaderBase
{
uint8_t typeAndFlags;
virtual PacketType getType() { return static_cast<PacketType(typeAndFlags >> 4); }
virtual uint8 getFlags() { return 0; } // Most common code here
virtual bool parseType(const uint8_t * buffer, int len)
{
if (len < 1) return false;
typeAndFlags = buffer[0];
return true;
}
virtual ~FixedHeaderBase() {}
};
// So that most class ends up empty
template <>
struct FixedHeader<CONNECT> final : public FixedHeaderBase
{
};
// And specialize only the specific classes
template <>
struct FixedHeader<PUBLISH> final : public FixedHeaderBase
{
uint8 getFlags() const { return typeAndFlags & 0xF; }
bool parseType(const uint8_t * buffer, int len)
{
if (!FixedHeaderBase::parseType(buffer, len)) return false;
if (typeAndFlags & 0x1) return false; // Example of per packet specific check to perform
return true;
}
};
// Most of the code is shared here
struct ControlPacketBase
{
FixedHeaderBase & type;
...etc ...
virtual bool parsePacket(const uint8_t * packet, int packetLen)
{
if (!type.parseType(packet, packetLen)) return false;
...etc ...
}
ControlPacketBase(FixedHeaderBase & type, etc...) : type(type) {}
virtual ~ControlPacketBase() {}
};
// This is only there to tell which specific version to use for the generic code
template <PacketType type>
struct ControlPacket final : public ControlPacketBase
{
FixedHeader<type> type;
VariableHeader<type> header;
Properties<type> props;
... and so on...
ControlPacket() : ControlPacketBase(type, header, props, etc...) {}
};
This is working quite well and allows to shave a lot of the used binary code space. By the way, I'm using final
here so the compiler could devirtualize, and I'm compiling without RTTI (obviously also with -Os and each function in its own section that are garbage collected).
However, when I inspect the symbol table size, I'm finding a lot of duplication on the destructors (all template instances are implementing a destructor which is clearly the same (same binary size) or empty).
Typically, I understand that ControlPacket<CONNECT>
needs to call ~FixedHeader<CONNECT>()
and that ControlPacket<PUBLISH>
needs to call ~FixedHeader<PUBLISH>()
upon destruction.
Yet, since all the destructor are virtual, is there a way that the specialization of ControlPacket
avoid their destructors and instead have ControlPacketBase
to destruct them virtually so that I don't ends up with 16 useless destructors but only one ?