首页 > 解决方案 > 如何减少当前序列化所需的样板

问题描述

我们的软件正在抽象出硬件,我们有代表这个硬件状态的类,并且有很多用于该外部硬件所有属性的数据成员。我们需要定期更新有关该状态的其他组件,为此我们通过 MQTT 和其他消息传递协议发送 protobuf 编码的消息。有不同的消息描述硬件的不同方面,因此我们需要发送这些类数据的不同视图。这是一个草图:

struct some_data {
  Foo foo;
  Bar bar;
  Baz baz;
  Fbr fbr;
  // ...
};

假设我们需要发送一条包含fooand的消息bar,以及一条包含barand的消息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)) );
}

我得到了这个工作,它大大减少了样板。(不是在这个小例子中,但如果你想象有十几个数据点被编码和解码,很多重复的数据成员列表消失会产生很大的不同)。但是,这有两个缺点:

  1. 这依赖于FooBarBaz是不同的类型。如果它们都是int,我们需要在元组中添加一个虚拟标签类型。

    这是可以做到的,但它确实使整个想法的吸引力大大降低。

  2. 旧代码中的变量名变成了新代码中的注释和数字。这很糟糕,并且考虑到混淆两个成员的错误很可能出现在编码和解码中,它不能在简单的单元测试中捕获,但需要通过其他技术创建的测试组件(所以集成测试)用于捕获此类错误。

    我不知道如何解决这个问题。

有没有人更好地了解如何为我们减少样板文件?

笔记:

标签: c++boilerplateredundancystdtuple

解决方案


在我看来,最好的全方位解决方案是使用脚本语言编写的外部 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 细节(至少在当时),但它们足以为我们的所有类型(包括模板类、容器等)生成所需的序列化代码。


推荐阅读