Как уменьшить шаблон в настоящее время необходимо для сериализации

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

28

Решение

Я буду опираться на предложенное вами решение, но вместо этого используйте 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);
}

Код Вот

РЕДАКТИРОВАТЬ:

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

Обновленный код Вот

7

Другие решения

На мой взгляд, лучшим универсальным решением является внешний генератор кода 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 (по крайней мере, на тот момент), их было достаточно для генерации необходимого кода сериализации для всех наших типов (которые включают в себя шаблонные классы, контейнеры и т. Д.).

11

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

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

#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;
}
};
3

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

2