Since c++17 you can use std::variant
for a tagged union and if c++17 is not an option for you, then pick boost::variant
as you are already using boost.
Here is an example how to use a variant to send and receive messages between two processes or even over network. I used concepts
which is available since c++20 to produce better error messages but you can do without them.
#include <variant>
#include <cassert> // for test
class BufferWriter;
class BufferReader;
// concepts for better error messages
template<class T>
concept Writable = requires(BufferWriter& writer, const T& msg) {
writer << msg;
};
template<class T>
concept Readable = requires(BufferReader& reader, T& msg) {
reader >> msg;
};
class BufferWriter {
// your impl here
public:
template<Writable Msg>
void write(const Msg& msg) {
*this << msg;
}
};
class BufferReader {
// your impl here
public:
template<Readable Msg>
void read(Msg& msg) {
*this >> msg;
}
template<Readable Msg>
Msg read() {
Msg msg;
*this >> msg;
return msg;
}
};
// for integral as the index is of integer type
template<std::integral I>
BufferWriter& operator<<(BufferWriter& writer, I msg);
template<std::integral I>
BufferReader& operator>>(BufferReader& reader, I msg);
template<Writable ... Msgs>
BufferWriter& operator<<(BufferWriter& writer, const std::variant<Msgs...>& msgs) {
writer << msgs.index();
std::visit([&writer](const auto& msg) { writer << msg; }, msgs);
return writer;
}
template<std::size_t I, class Var>
bool try_read_one_var_msg(BufferReader& reader, Var& var, std::size_t i) {
if (i == I) {
using msg_type = std::variant_alternative_t<I, Var>;
var.template emplace<I>(reader.read<msg_type>());
return true;
}
return false;
}
template<class Var, std::size_t ... IndexSeq>
void read_msg_of_variant(BufferReader& reader, Var& var, std::index_sequence<IndexSeq...>) {
auto index = reader.read<std::size_t>();
bool success = (try_read_one_var_msg<IndexSeq>(reader, var, index) || ...);
if (!success) // the index didn't match any variant index
throw std::bad_variant_access{};
}
template<Readable ... Msgs>
BufferReader& operator>>(BufferReader& reader, std::variant<Msgs...>& msgs) {
read_msg_of_variant(reader, msgs, std::index_sequence_for<Msgs...>{});
return reader;
}
struct Msg1 {
// your msg impl
};
struct Msg2 {
// your msg impl
};
// pack a message into the writer using a serialization protocol
BufferWriter& operator<<(BufferWriter& writer, const Msg1& msg);
BufferWriter& operator<<(BufferWriter& writer, const Msg2& msg);
// extract a message from the writer using a serialization protocol
BufferReader& operator>>(BufferReader& reader, const Msg1& msg);
BufferReader& operator>>(BufferReader& reader, const Msg2& msg);
int main() {
std::variant<Msg1, Msg2> msgs;
msgs.emplace<Msg2>();
BufferWriter writer;
BufferReader reader;
writer << msgs;
std::variant<Msg1, Msg2> msgs2;
reader >> msgs2;
assert(msgs2.index() == 1);
return 0;
}
I didn't include the serialization part because this is per application requirement. Also the compiler compiles this better than a hand written switch statement in some cases and I think at worst it will be identical to a switch or if. In the write method an exception will be thrown in std::visit
if the variant is valueless_by_exception
and in the read method an exception will be thrown if the index is not contained in the variant.
code and asm result on godbolt: https://godbolt.org/z/f4bM51x8E