Как уменьшить шаблон в настоящее время необходимо для сериализации - PullRequest
0 голосов
/ 14 мая 2018

Наше программное обеспечение абстрагируется от аппаратного обеспечения, и у нас есть классы, которые представляют состояние этого аппаратного обеспечения и имеют множество элементов данных для всех свойств этого внешнего аппаратного обеспечения. Нам необходимо регулярно обновлять другие компоненты об этом состоянии, и для этого мы отправляем сообщения в кодировке protobuf через MQTT и другие протоколы обмена сообщениями. Существуют разные сообщения, которые описывают различные аспекты аппаратного обеспечения, поэтому нам нужно отправлять разные представления данных этих классов. Вот эскиз:

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)) );
}

У меня это работает, и оно значительно сокращает шаблон. (Не в этом небольшом примере, но если вы представляете, что дюжина точек данных кодируется и декодируется, множество повторяющихся списков исчезающих элементов данных имеет большое значение). Однако это имеет два недостатка:

  1. Это зависит от Foo, Bar и Baz, являющихся различными типами. Если они все int, нам нужно добавить фиктивный тип тега в кортеж.

    Это можно сделать, но это делает эту идею значительно менее привлекательной.

  2. То, что имена переменных в старом коде становятся комментариями и числами в новом коде. Это довольно плохо, и, учитывая, что вполне вероятно, что в кодировке, а также в декодировании, вероятно, присутствует ошибка, приводящая к путанице в двух элементах, она не может быть обнаружена в простых модульных тестах, но нуждается в тестовых компонентах, созданных с помощью других технологий (поэтому интеграционные тесты) для ловли таких багов.

    Понятия не имею, как это исправить.

Кто-нибудь лучше знает, как уменьшить шаблон для нас?

Примечание:

  • Пока что мы застряли на C ++ 03. Да, вы правильно прочитали. Для нас это std::tr1::tuple. Нет лямбды. И нет auto либо.
  • У нас есть тонна кода, использующего эти черты сериализации. Мы не можем выбросить всю схему и сделать что-то совершенно другое. Я ищу решение для упрощения будущей подгонки кода к существующей инфраструктуре. Любая идея, которая требует от нас переписать все это, скорее всего, будет отклонена.

Ответы [ 5 ]

0 голосов
/ 20 июня 2018

Рассматривали ли вы немного другой подход? Вместо того, чтобы иметь отдельное представление FooBar и BarBaz, рассмотрим FooBarBaz, похожий на

message 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 от эксплицитных наборов полей к обычным сообщениям с необязательными полями, чтобы отправлять только то, что вам нужно. Возможно, вы захотите обернуть края вашей системы, чтобы утверждать, что поля, требуемые для определенного процесса, установлены, прежде чем пытаться использовать это, но это может привести к уменьшению количества шаблонов в других местах. Уловка конкатенации строк также может быть полезна в тех случаях, когда вы проходите через систему, которой на самом деле все равно, что в них.

0 голосов
/ 13 июня 2018

Я буду опираться на предложенное вами решение, но вместо этого используйте boost :: fusion :: tuples (при условии, что это разрешено). Давайте предположим, что ваши типы данных

struct Foo{};
struct Bar{};
struct Baz{};
struct Fbr{};

и ваши данные

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

Из комментариев я понимаю, что вы не контролируете классы 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;
    }
};

Теперь мы можем уменьшить шаблон и ограничить его одним typedef для каждой перестановки типов данных и одной простой перегрузкой для каждого члена set_XXX класса SerializedXYZ следующим образом:

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);
}
//...

Хорошо, что вам больше не нужно специализировать свои 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;
    }
};

А вот несколько примеров того, как это работает. Обратите внимание, что если кто-то попытается связать элемент данных из 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);
}

Код здесь

EDIT:

К сожалению, это решение не решает проблему № 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);
}

Шаблон снова ограничен одним 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;
    }
};

и вот как это работает:

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())));
}

Обновленный код здесь

0 голосов
/ 15 мая 2018

То, что вы хотите - это то, что подобно кортежу , но не является настоящим кортежем. Предполагая, что все 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 () в терминах его членов.

0 голосов
/ 10 июня 2018

Если ваш базовый шаблон действительно представляет собой просто набор простых старых структур данных с тривиальными операторами сравнения, вы, вероятно, можете избежать некоторых макросов.

#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 для именования получателей кортежа.

#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;
    }
};
0 голосов
/ 14 мая 2018

На мой взгляд, лучшим универсальным решением является внешний генератор кода C ++ на языке сценариев.Он имеет следующие преимущества:

  • Гибкость : позволяет изменять сгенерированный код в любое время.Это очень хорошо по нескольким причинам:

    • Легко исправляйте ошибки во всех старых поддерживаемых выпусках.
    • Используйте новые функции C ++, если вы переходите на C ++ 11 или более позднюю версию вбудущее.
    • Создание кода для другого языка.Это очень, очень полезно (особенно если ваша организация большая и / или у вас много пользователей).Например, вы можете вывести небольшую библиотеку сценариев (например, модуль Python), которую можно использовать как инструмент CLI для взаимодействия с оборудованием.По моему опыту, это очень понравилось инженерам.
    • Создание кода GUI (или описаний GUI, например, в XML / JSON или даже веб-интерфейса) - полезно для людей, использующих конечное оборудование и тестировщиков.
    • Генерация данных другого типа.Например, диаграммы, статистика и т. Д. Или даже сами описания протобуфов.
  • Обслуживание : его будет легче поддерживать, чем в C ++.Даже если он написан на другом языке, обычно легче выучить этот язык, чем позволить новому разработчику C ++ погрузиться в метапрограммирование шаблонов C ++ (особенно в C ++ 03).

  • Производительность : он может легко сократить время компиляции на стороне C ++ (поскольку вы можете вывести очень простой C ++ - даже простой C).Конечно, генератор может компенсировать это преимущество.В вашем случае это может не применяться, так как похоже, что вы не можете изменить код клиента.

Я использовал этот подход в паре проектов / систем, и это оказалось довольно неплохо,Специально различные альтернативы для использования аппаратного обеспечения (C ++ lib, Python lib, CLI, GUI ...) могут быть очень признаны.


Примечание: если часть генерации требует синтаксического анализа уже существующего кода C ++ (например, заголовки с сериализуемыми типами данных, как в случае OP с типами Serialized);тогда очень хорошее решение - использовать инструмент LLVM / clang для этого.

В конкретном проекте, над которым я работал, нам пришлось автоматически сериализовать десятки типов C ++ (которые могли быть измененыв любое время пользователями).Нам удалось автоматически сгенерировать код для него, просто используя привязки Python и включив его в процесс сборки.Хотя привязки Python не раскрывают все детали AST (по крайней мере, в то время), их было достаточно для генерации необходимого кода сериализации для всех наших типов (включая шаблонные классы, контейнеры и т. Д.).

...