c++ - 如何减少当前序列化所需的样板
问题描述
我们的软件正在抽象出硬件,我们有代表这个硬件状态的类,并且有很多用于该外部硬件所有属性的数据成员。我们需要定期更新有关该状态的其他组件,为此我们通过 MQTT 和其他消息传递协议发送 protobuf 编码的消息。有不同的消息描述硬件的不同方面,因此我们需要发送这些类数据的不同视图。这是一个草图:
struct some_data {
Foo foo;
Bar bar;
Baz baz;
Fbr fbr;
// ...
};
假设我们需要发送一条包含foo
and的消息bar
,以及一条包含bar
and的消息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
,我们需要在元组中添加一个虚拟标签类型。这是可以做到的,但它确实使整个想法的吸引力大大降低。
旧代码中的变量名变成了新代码中的注释和数字。这很糟糕,并且考虑到混淆两个成员的错误很可能出现在编码和解码中,它不能在简单的单元测试中捕获,但需要通过其他技术创建的测试组件(所以集成测试)用于捕获此类错误。
我不知道如何解决这个问题。
有没有人更好地了解如何为我们减少样板文件?
笔记:
- 目前,我们还停留在 C++03 上。是的,你没有看错。对我们来说,是
std::tr1::tuple
。没有拉姆达。也没有auto
。 - 我们有大量使用这些序列化特征的代码。我们不能抛弃整个计划,做一些完全不同的事情。我正在寻找一种解决方案来简化未来代码以适应现有框架。任何需要我们重写整个内容的想法很可能会被驳回。
解决方案
在我看来,最好的全方位解决方案是使用脚本语言编写的外部 C++ 代码生成器。它具有以下优点:
灵活性:它允许您随时更改生成的代码。这对于几个子原因非常有用:
- 随时修复所有旧受支持版本中的错误。
- 如果您将来迁移到 C++11 或更高版本,请使用新的 C++ 功能。
- 为不同的语言生成代码。这非常非常有用(特别是如果您的组织很大和/或您有很多用户)。例如,您可以输出一个小的脚本库(例如 Python 模块),它可以用作 CLI 工具来与硬件接口。根据我的经验,这很受硬件工程师的欢迎。
- 生成 GUI 代码(或 GUI 描述,例如 XML/JSON;甚至是 Web 界面)——对使用最终硬件和测试人员的人很有用。
- 生成其他类型的数据。例如,图表、统计数据等。甚至是 protobuf 描述本身。
维护:比 C++ 更容易维护。即使它是用不同的语言编写的,学习该语言通常也比让新的 C++ 开发人员深入研究 C++ 模板元编程(特别是在 C++03 中)更容易。
性能:它可以轻松减少 C++ 端的编译时间(因为您可以输出非常简单的 C++ -- 甚至是纯 C)。当然,生成器可能会抵消这一优势。在您的情况下,这可能不适用,因为您似乎无法更改客户端代码。
我在几个项目/系统中使用了这种方法,结果非常好。特别是使用硬件的不同替代方案(C++ lib、Python lib、CLI、GUI ...)可以非常感激。
旁注:如果生成的一部分需要解析已经存在的C++ 代码(例如,具有要序列化的数据类型的标头,就像在 OP 的情况下使用Serialized
类型一样);那么一个非常好的解决方案是使用LLVM/clang 的工具来做到这一点。
在我从事的一个特定项目中,我们必须自动序列化数十种 C++ 类型(用户随时可能更改)。我们设法通过使用 clang Python 绑定自动为其生成代码并将其集成到构建过程中。虽然 Python 绑定没有公开所有 AST 细节(至少在当时),但它们足以为我们的所有类型(包括模板类、容器等)生成所需的序列化代码。
推荐阅读
- sql - 创建干净的 Oracle SQL 视图而不在每一行中重复相同的信息
- sql - 查询某些字段时,只返回每个字段都有数据的用户
- javascript - 有没有办法在 Vue.js 中动态插入点击事件?
- spring-cloud-netflix - 如何将 Hystrix 与 Spring WebFlux WebClients 一起使用?
- node.js - Express res.send mongoose result to ajax
- azure - 将 Microsoft Authenticator 与 Web 应用程序集成
- python - How do I import a module created with pybind11 on Ubuntu
- python - 等待后无法杀死子进程
- python - XgBoost 精度结果在每次运行时都不同,但参数相同。我怎样才能使它们保持不变?
- node.js - 为数组中的每个元素添加唯一值