Our software is abstracting away hardware, and we have classes that represent this hardware's state and have lots of data members for all properties of that external hardware. We need to regularly update other components about that state, and for that we send protobuf-encoded messages via MQTT and other messaging protocols. There are different messages that describe different aspects of the hardware, so we need to send different views of the data of those classes. Here's a sketch:
struct some_data {
Foo foo;
Bar bar;
Baz baz;
Fbr fbr;
// ...
};
Let's assume we need to send one message containing foo
and bar
, and one containing bar
and baz
. Our current way of doing this is a lot of boiler-plate:
struct foobar {
Foo foo;
Bar bar;
foobar(const Foo& foo, const Bar& bar) : foo(foo), bar(bar) {}
bool operator==(const foobar& rhs) const {return foo == rhs.foo && bar == rhs.bar;}
bool operator!=(const foobar& rhs) const {return !operator==(*this,rhs);}
};
struct barbaz {
Bar bar;
Baz baz;
foobar(const Bar& bar, const Baz& baz) : bar(bar), baz(baz) {}
bool operator==(const barbaz& rhs) const {return bar == rhs.bar && baz == rhs.baz;}
bool operator!=(const barbaz& rhs) const {return !operator==(*this,rhs);}
};
template<> struct serialization_traits<foobar> {
static SerializedFooBar encode(const foobar& fb) {
SerializedFooBar sfb;
sfb.set_foo(fb.foo);
sfb.set_bar(fb.bar);
return sfb;
}
};
template<> struct serialization_traits<barbaz> {
static SerializedBarBaz encode(const barbaz& bb) {
SerializedBarBaz sbb;
sfb.set_bar(bb.bar);
sfb.set_baz(bb.baz);
return sbb;
}
};
This can then be sent:
void send(const some_data& data) {
send_msg( serialization_traits<foobar>::encode(foobar(data.foo, data.bar)) );
send_msg( serialization_traits<barbaz>::encode(barbaz(data.foo, data.bar)) );
}
Given that the data sets to be sent are often much larger than two items, that we need to decode that data, too, and that we have tons of these messages, there is a lot more boilerplate involved than what's in this sketch. So I have been searching for a way to reduce this. Here's a first idea:
typedef std::tuple< Foo /* 0 foo */
, Bar /* 1 bar */
> foobar;
typedef std::tuple< Bar /* 0 bar */
, Baz /* 1 baz */
> barbaz;
// yay, we get comparison for free!
template<>
struct serialization_traits<foobar> {
static SerializedFooBar encode(const foobar& fb) {
SerializedFooBar sfb;
sfb.set_foo(std::get<0>(fb));
sfb.set_bar(std::get<1>(fb));
return sfb;
}
};
template<>
struct serialization_traits<barbaz> {
static SerializedBarBaz encode(const barbaz& bb) {
SerializedBarBaz sbb;
sfb.set_bar(std::get<0>(bb));
sfb.set_baz(std::get<1>(bb));
return sbb;
}
};
void send(const some_data& data) {
send_msg( serialization_traits<foobar>::encode(std::tie(data.foo, data.bar)) );
send_msg( serialization_traits<barbaz>::encode(std::tie(data.bar, data.baz)) );
}
I got this working, and it cuts the boilerplate considerably. (Not in this small example, but if you imagine a dozen data points being encoded and decoded, a lot of the repeating listings of data members disappearing makes a lot of difference). However, this has two disadvantages:
This relies on
Foo
,Bar
, andBaz
being distinct types. If they are allint
, we need to add a dummy tag type to the tuple.This can be done, but it does make this whole idea considerably less appealing.
What's variable names in the old code becomes comments and numbers in the new code. That's pretty bad, and given that it is likely that a bug confusing two members is likely present in the encoding as well as in the decoding, it can't be caught in simple unit tests, but needs test components created through other technologies (so integration tests) for catching such bugs.
I have no idea how to fix this.
Has anybody a better idea how to reduce the boilerplate for us?
Note:
- For the time being, we're stuck with C++03. Yes, you read that right. For us, it's
std::tr1::tuple
. No lambda. And noauto
either. - We have a tons of code employing those serialization traits. We cannot throw away the whole scheme and do something completely different. I am looking for a solution to simplify future code fitting into the existing framework. Any idea that requires us to re-write the whole thing will very likely be dismissed.