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.
:-)
– ProsserSerializedFooBar
to be a protobuf-generated type. Now what? – Prosserobj.SerializeToOstream(&output)
orobj.ParseFromIstream(&input)
anySerializedFooBar obj;
. What do you expect? – Precipitationsome_data
itself? – Golankasome_data
. But that's simplified. There are also messages that combine members of different classes into one message. – Prosserfoo
andbar
combined to a foobar message? Are this data sets somehow related? To me it looks more like the boilerplate is just an unnecessary combination of data?! Also, are you able to slightly alter the data structures (struct Foo, Bar etc.) e.g. add a function to it? And how do you decode the messages? – HutchisonSerialized...
classes are generated. I have no control over them at all. – Prosserserialization_traits<foo>
andserialization_traits<bar>
are the same type, whenfoo
andbar
are tuples with the same lists of types. That those type lists are semantically different doesn't matter for the syntax. – ProsserSerializedXY
types, allowing access to them through a simplified interface (your choice being the specializations ofserialization_traits
). Is that a fair summary? From this perspective, the stuff you've tried is useful as an example of what you are looking for, but more useful would be information about theseSerializedXY
types. Could you add to your question some information about them, such as how they are generated (why this cannot change) and what their public interface is? – GroffSerialized...
types are generated from some IDL. Currently, most of them are protobuf-generated while some are JSON-containers, but that might change. We want to keep them out of our code, as we have little control over what they look like and because they might change. That's one of the reasons for this translation layer for copying between our internal data and them. – ProsserSerializedFooBar
class would rename itsset_foo
member to simplyfoo
while the otherSerializedFooY
classes retainset_foo
. Is this a possibility? If so, that is critical information in that it invalidates some approaches. If not, that is some information about their public interface (as I requested). What do you have control over? What can be assumed about them? Why do they have this nice uniformSerializedXY
naming scheme? Is that subject to outside changes? – GroffSerialized...
types are generated from our IDL, so we do have some control. However, they aren't generated directly (currently we generate protobuf files, from which C++ is generated) and their exact interface depends on the interface which might change any time. So, yes,set_foo()
might change. That's why we have the serialization traits, after all: they are supposed to isolate our code from those external interfaces. I just want it a little bit more declarative, and less repetitive. – Prosser