我们的软件抽象出硬件,我们有代表这个硬件状态的类,并且拥有许多用于该外部硬件属性的数据成员。我们需要定期更新有关该状态的其他组件,为此我们通过MQTT和其他消息传递协议发送protobuf编码的消息。有不同的消息描述硬件的不同方面,因此我们需要发送这些类的数据的不同视图。这是一个草图:
struct some_data {
Foo foo;
Bar bar;
Baz baz;
Fbr fbr;
// ...
};
我们假设我们需要发送一条包含foo
和bar
的邮件,以及一条包含bar
和baz
的邮件。我们目前的做法是大量的锅炉板:
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;
}
};
然后可以发送:
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)) );
}
鉴于要发送的数据集通常比两个项目大得多,我们也需要对这些数据进行解码,并且我们有大量的这些消息,所以涉及的内容比这个草图中的内容要多得多。 。所以我一直在寻找减少这种方法的方法。这是第一个想法:
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)) );
}
我得到了这个工作,它大大削减了样板。 (不是在这个小例子中,但如果你想象十几个数据点被编码和解码,很多重复的数据成员列表消失会产生很大的不同)。但是,这有两个缺点:
这取决于Foo
,Bar
和Baz
是不同的类型。如果它们都是int
,我们需要在元组中添加一个虚拟标记类型。
这可以做到,但它确实使整个想法不那么吸引人。
旧代码中的变量名称变为新代码中的注释和数字。这非常糟糕,并且鉴于编码和解码中可能存在混淆两个成员的错误,它不能在简单的单元测试中捕获,但需要通过其他技术创建的测试组件(所以用于捕获此类错误的集成测试。
我不知道如何解决这个问题。
有谁更好地了解如何为我们减少样板?
注意:
std::tr1::tuple
。没有lambda。也没有auto
。答案 0 :(得分:11)
在我看来,最好的全能解决方案是脚本语言中的外部C ++代码生成器。它具有以下优点:
灵活性:它允许您随时更改生成的代码。由于以下几个原因,这非常好:
维护:它比C ++更容易维护。即使它是用不同的语言编写的,学习该语言通常比让新的C ++开发人员更深入地学习C ++模板元编程(特别是在C ++ 03中)。
性能:它可以轻松减少C ++端的编译时间(因为您可以输出非常简单的C ++ - 甚至是纯C)。当然,发电机可以抵消这种优势。在您的情况下,这可能不适用,因为看起来您无法更改客户端代码。
我在几个项目/系统中使用过这种方法,结果非常好。特别是使用硬件的不同替代方案(C ++ lib,Python lib,CLI,GUI ...)可以 非常 赞赏。
附注:如果生成的一部分需要解析已经存在的 C ++代码(例如,要序列化数据类型的标题,就像在OP {&#39}}类型的情况下那样);那么一个非常好的解决方案就是使用LLVM/clang's tooling来实现这一目标。
在我工作的一个特定项目中,我们必须自动序列化几十种C ++类型(用户随时都可以更改)。我们设法通过使用clang Python绑定自动生成代码,并将其集成到构建过程中。虽然Python绑定没有暴露所有AST细节(至少在当时),但它们足以为我们所有类型(包括模板化类,容器等)生成所需的序列化代码。
答案 1 :(得分:7)
我将基于您提出的解决方案,但使用boost :: fusion :: tuples(假设允许)。我们假设您的数据类型是
struct some_data {
Foo foo;
Bar bar;
Baz baz;
Fbr fbr;
};
,您的数据是
struct SerializedFooBar {
void set_foo(const Foo&){
std::cout << "set_foo in SerializedFooBar" << std::endl;
}
void set_bar(const Bar&){
std::cout << "set_bar in SerializedFooBar" << std::endl;
}
};
// another protobuf-generated class
struct SerializedBarBaz {
void set_bar(const Bar&){
std::cout << "set_bar in SerializedBarBaz" << std::endl;
}
void set_baz(const Baz&){
std::cout << "set_baz in SerializedBarBaz" << std::endl;
}
};
从评论中,我了解到您无法控制SerialisedXYZ类,但它们确实有一定的界面。我会假设这样的东西足够接近(?):
typedef boost::fusion::tuple<Foo, Bar> foobar;
typedef boost::fusion::tuple<Bar, Baz> barbaz;
//...
template <class S>
void serialized_set(S& s, const Foo& v) {
s.set_foo(v);
}
template <class S>
void serialized_set(S& s, const Bar& v) {
s.set_bar(v);
}
template <class S>
void serialized_set(S& s, const Baz& v) {
s.set_baz(v);
}
template <class S, class V>
void serialized_set(S& s, const Fbr& v) {
s.set_fbr(v);
}
//...
我们现在可以减少样板并将其限制为每个数据类型排列一个typedef和SerializedXYZ类的每个set_XXX成员的一个简单重载,如下所示:
template <class SerializedX>
class serialization_traits {
struct set_functor {
template <class V>
SerializedX& operator()(SerializedX& s, const V& v) const {
serialized_set(s, v);
return s;
}
};
public:
template <class Tuple>
static SerializedX encode(const Tuple& t) {
SerializedX s;
boost::fusion::fold(t, s, set_functor());
return s;
}
};
现在好处是你不再需要专门化你的serialization_traits了。以下使用boost :: fusion :: fold函数,我假设您可以在项目中使用它:
void send_msg(const SerializedFooBar&){
std::cout << "Sent SerializedFooBar" << std::endl;
}
void send_msg(const SerializedBarBaz&){
std::cout << "Sent SerializedBarBaz" << std::endl;
}
void send(const some_data& data) {
send_msg( serialization_traits<SerializedFooBar>::encode(boost::fusion::tie(data.foo, data.bar)) );
send_msg( serialization_traits<SerializedBarBaz>::encode(boost::fusion::tie(data.bar, data.baz)) );
// send_msg( serialization_traits<SerializedFooBar>::encode(boost::fusion::tie(data.foo, data.baz)) ); // compiler error; SerializedFooBar has no set_baz member
}
int main() {
some_data my_data;
send(my_data);
}
以下是一些如何运作的例子。请注意,如果有人试图绑定some_data中不符合SerializedXYZ接口的数据成员,编译器会通知您:
serialized_set
代码here
修改强>
不幸的是,这个解决方案并没有解决OP的问题#1。为了解决这个问题,我们可以为每个数据成员定义一系列标记,并遵循类似的方法。以下是标记以及修改后的struct foo_tag{};
struct bar1_tag{};
struct bar2_tag{};
struct baz_tag{};
struct fbr_tag{};
template <class S>
void serialized_set(S& s, const some_data& data, foo_tag) {
s.set_foo(data.foo);
}
template <class S>
void serialized_set(S& s, const some_data& data, bar1_tag) {
s.set_bar1(data.bar1);
}
template <class S>
void serialized_set(S& s, const some_data& data, bar2_tag) {
s.set_bar2(data.bar2);
}
template <class S>
void serialized_set(S& s, const some_data& data, baz_tag) {
s.set_baz(data.baz);
}
template <class S>
void serialized_set(S& s, const some_data& data, fbr_tag) {
s.set_fbr(data.fbr);
}
函数:
serialized_set
对于每个数据成员,样板文件再次限制为一个// the serialization_traits doesn't need specialization anymore :)
template <class SerializedX>
class serialization_traits {
class set_functor {
const some_data& m_data;
public:
typedef SerializedX& result_type;
set_functor(const some_data& data)
: m_data(data){}
template <class Tag>
SerializedX& operator()(SerializedX& s, Tag tag) const {
serialized_set(s, m_data, tag);
return s;
}
};
public:
template <class Tuple>
static SerializedX encode(const some_data& data, const Tuple& t) {
SerializedX s;
boost::fusion::fold(t, s, set_functor(data));
return s;
}
};
并且线性扩展,类似于我之前的答案。这是修改后的serialization_traits:
void send(const some_data& data) {
send_msg( serialization_traits<SerializedFooBar>::encode(data,
boost::fusion::make_tuple(foo_tag(), bar1_tag())));
send_msg( serialization_traits<SerializedBarBaz>::encode(data,
boost::fusion::make_tuple(baz_tag(), bar1_tag(), bar2_tag())));
}
以下是它的工作原理:
/p:DeployOnBuild=true
更新了代码here
答案 2 :(得分:3)
你想要的是 tuple-like 但不是真正的元组。假设所有tuple_like
类实现tie()
基本上只是绑定其成员,这是我的假设代码:
template<typename T> struct tuple_like {
bool operator==(const T& rhs) const {
return this->tie() == rhs.tie();
}
bool operator!=(const T& rhs) const {
return !operator==(*this,rhs);
}
};
template<typename T, typename Serialised> struct serialised_tuple_like : tuple_like<T> {
};
template<typename T, typename Serialised>
struct serialization_traits<serialised_tuple_like<T, Serialised>> {
static Serialised encode(const T& bb) {
Serialised s;
s.tie() = bb.tie();
return s;
}
};
只要双方都实施了合适的领带(),这应该没问题。如果源类或目标类不直接在您的控件中,建议定义一个实现tie()的继承类并使用它。要合并多个类,请定义一个辅助类,该类根据其成员实现tie()。
答案 3 :(得分:3)
如果你的样板实际上只是一堆普通的旧数据结构,并且有一些简单的比较运算符,你可能会得到一些宏。
#define POD2(NAME, T0, N0, T1, N1) \
struct NAME { \
T0 N0; \
T1 N1; \
NAME(const T0& N0, const T1& N1) \
: N0(N0), N1(N1) {} \
bool operator==(const NAME& rhs) const { return N0 == rhs.N0 && N1 == rhs.N1; }
\
bool operator!=(const NAME& rhs) const { return !operator==(rhs); } \
};
用法如下:
POD2(BarBaz, Bar, bar, Baz, baz)
template <>
struct serialization_traits<BarBaz> {
static SerializedBarBaz encode(const BarBaz& bb) {
SerializedBarBaz sbb;
sbb.set_bar(bb.bar);
sbb.set_baz(bb.baz);
return sbb;
}
};
您需要N个宏,其中N是您拥有的参数计数的排列数,但这将是一次性的前期成本。
或者你可以像你建议的那样利用元组为你做很多繁重的工作。在这里,我创建了一个“NamedTuple”模板,用于命名元组的getter。
#define NAMED_TUPLE2_T(N0, N1) NamedTuple##N0##N1
#define NAMED_TUPLE2(N0, N1) \
template <typename T0, typename T1> \
struct NAMED_TUPLE2_T(N0, N1) { \
typedef std::tuple<T0, T1> TupleType; \
const typename std::tuple_element<0, TupleType>::type& N0() const { return std::get<0>(tuple_); } \
const typename std::tuple_element<1, TupleType>::type& N1() const { return std::get<1>(tuple_); } \
NAMED_TUPLE2_T(N0, N1)(const std::tuple<T0, T1>& tuple) : tuple_(tuple) {} \
bool operator==(const NAMED_TUPLE2_T(N0, N1)& rhs) const { return tuple_ == rhs.tuple_; } \
bool operator!=(const NAMED_TUPLE2_T(N0, N1)& rhs) const { return !operator==(rhs); } \
private: \
TupleType tuple_; \
}; \
typedef NAMED_TUPLE2_T(N0, N1)
用法:
NAMED_TUPLE2(foo, bar)<int, int> FooBar;
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;
}
};
答案 4 :(得分:2)
您是否考虑过一种稍微不同的方法?与其使用单独的FooBar和BarBaz表示,不如考虑类似
的FooBarBazmessage FooBarBaz {
optional Foo foo = 1;
optional Bar bar = 2;
optional Baz baz = 3;
}
然后在您的应用程序代码中,您可以像这样利用它:
FooBarBaz foo;
foo.set_foo(...);
FooBarBaz bar;
bar.set_bar(...);
FooBarBaz baz;
baz.set_baz(...);
FooBarBaz foobar = foo;
foobar.MergeFrom(bar);
FooBarBaz barbaz = bar;
barbaz.MergeFrom(baz);
或者,您可以利用protobuf编码并序列化消息。 (protobuf本身实际上并没有序列化,您可以通过在其上调用ToString方法之一来实现)。
// assume string_foo is the actual serialized foo from above, likewise string_bar
string serialized_foobar = string_foo + string_bar;
string serialized_barbaz = string_bar + string_baz;
FooBarBaz barbaz;
barbaz.ParseFromString(serialized_barbaz);
这确实假定您可以将大多数api从显式字段集移到带有可选字段的常见消息上,仅发送所需的内容。您可能需要包装系统的边缘,以断言在尝试使用特定过程之前已设置了特定过程所需的字段,但这可能导致其他地方的样板减少。如果您通过的系统实际上并不在乎其中的内容,那么字符串concat技巧也很方便。