如何减少当前序列化所需的样板

sbi*_*sbi 28 c++ redundancy boilerplate stdtuple

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

struct some_data {
  Foo foo;
  Bar bar;
  Baz baz;
  Fbr fbr;
  // ...
};
Run Code Online (Sandbox Code Playgroud)

让我们假设我们需要发送一条包含foo和的消息bar,并且一封包含barbaz.我们目前的做法是大量的锅炉板:

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;
  }
};
Run Code Online (Sandbox Code Playgroud)

然后可以发送:

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)) );
}
Run Code Online (Sandbox Code Playgroud)

鉴于要发送的数据集通常远大于两个项目,我们也需要对这些数据进行解码,并且我们有大量这些消息,所以涉及的内容比本草图中的要多得多.所以我一直在寻找减少这种方法的方法.这是第一个想法:

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)) );
}
Run Code Online (Sandbox Code Playgroud)

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

  1. 这依赖于Foo,Bar并且Baz是不同的类型.如果它们都是int,我们需要在元组中添加一个虚拟标记类型.

    这可以做到,但它确实使整个想法不那么吸引人.

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

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

有谁更好地了解如何为我们减少样板?

注意:

  • 目前,我们仍然坚持使用C++ 03.是的,你没有看错.对我们来说,就是这样std::tr1::tuple.没有lambda.也没有auto.
  • 我们有大量使用这些序列化特征的代码.我们不能抛弃整个计划,做一些完全不同的事情.我正在寻找一种解决方案,以简化未来适合现有框架的代码.任何要求我们重写整件事的想法很可能会被驳回.

Aco*_*orn 12

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


lin*_*ver 7

我将基于您提出的解决方案,但使用boost :: fusion :: tuples(假设允许).我们假设您的数据类型是

struct Foo{};
struct Bar{};
struct Baz{};
struct Fbr{};
Run Code Online (Sandbox Code Playgroud)

你的数据是

struct some_data {
    Foo foo;
    Bar bar;
    Baz baz;
    Fbr fbr;
};
Run Code Online (Sandbox Code Playgroud)

从评论中,我了解到您无法控制SerialisedXYZ类,但它们确实有一定的接口.我会假设这样的东西足够接近(?):

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;
    }
};
Run Code Online (Sandbox Code Playgroud)

我们现在可以减少样板并将其限制为每个数据类型排列一个typedef和SerializedXYZ类的每个set_XXX成员的一个简单重载,如下所示:

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);
}
//...
Run Code Online (Sandbox Code Playgroud)

现在好处是你不再需要专门化你的serialization_traits了.以下使用boost :: fusion :: fold函数,我认为可以在项目中使用:

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;
    }
};
Run Code Online (Sandbox Code Playgroud)

以下是一些如何运作的例子.请注意,如果有人试图绑定some_data中不符合SerializedXYZ接口的数据成员,编译器会通知您:

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);
}
Run Code Online (Sandbox Code Playgroud)

代码在这里

编辑:

不幸的是,这个解决方案并没有解决OP的问题#1.为了解决这个问题,我们可以为每个数据成员定义一系列标记,并遵循类似的方法.以下是标签以及修改后的serialized_set功能:

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);
}
Run Code Online (Sandbox Code Playgroud)

该样板再次限制为serialized_set每个数据成员一个并且线性扩展,类似于我之前的答案.这是修改后的serialization_traits:

// 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;
    }
};
Run Code Online (Sandbox Code Playgroud)

以下是它的工作原理:

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())));
}
Run Code Online (Sandbox Code Playgroud)

这里更新了代码

  • 让我们[在聊天中继续讨论](https://chat.stackoverflow.com/rooms/173350/discussion-between-linuxfever-and-sbi). (2认同)