c++ - Cómo reducir la caldera actualmente necesaria para la serialización
boilerplate redundancy (5)
Nuestro software está abstrayendo el hardware, y tenemos clases que representan el estado de este hardware y tienen muchos miembros de datos para todas las propiedades de ese hardware externo. Necesitamos actualizar regularmente otros componentes sobre ese estado, y para eso enviamos mensajes codificados con protobuf a través de MQTT y otros protocolos de mensajería. Hay diferentes mensajes que describen diferentes aspectos del hardware, por lo que necesitamos enviar diferentes vistas de los datos de esas clases. Aquí hay un bosquejo:
struct some_data {
Foo foo;
Bar bar;
Baz baz;
Fbr fbr;
// ...
};
Supongamos que necesitamos enviar un mensaje que contenga foo
y bar
, y uno que contenga bar
y baz
. Nuestra forma actual de hacer esto es un montón de placa de caldera:
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;
}
};
Esto se puede enviar a continuación:
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)) );
}
Dado que los conjuntos de datos que se envían a menudo son mucho más grandes que dos elementos, que también necesitamos decodificar esos datos, y que tenemos toneladas de estos mensajes, hay mucho más contenido que el contenido de este boceto. Así que he estado buscando una manera de reducir esto. Aquí hay una primera idea:
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)) );
}
Conseguí este trabajo, y corta la placa de temperatura considerablemente. (No en este pequeño ejemplo, pero si imagina que una docena de puntos de datos se codifican y descodifican, muchos de los listados que se repiten de los miembros de datos que desaparecen hacen mucha diferencia). Sin embargo, esto tiene dos desventajas:
Esto se basa en que
Foo
,Bar
yBaz
son tipos distintos. Si todos sonint
, debemos agregar un tipo de etiqueta ficticia a la tupla.Esto se puede hacer, pero hace que toda esta idea sea considerablemente menos atractiva.
Los nombres de variables en el código antiguo se convierten en comentarios y números en el código nuevo. Eso es bastante malo, y dado que es probable que un error que confunda a dos miembros esté presente en la codificación y en la decodificación, no se puede detectar en pruebas unitarias simples, sino que se necesitan componentes de prueba creados a través de otras tecnologías pruebas de integración) para la captura de tales errores.
No tengo idea de cómo solucionar este problema.
¿Alguien tiene una mejor idea de cómo reducir la placa de cocción para nosotros?
Nota:
- Por el momento, estamos atrapados con C ++ 03. Sí, lo leiste bien. Para nosotros, es
std::tr1::tuple
. No lambda. Y tampocoauto
. - Tenemos toneladas de código que emplean esos rasgos de serialización. No podemos tirar todo el esquema y hacer algo completamente diferente. Estoy buscando una solución para simplificar el futuro ajuste de código en el marco existente. Cualquier idea que requiera que reescribamos todo será desestimada.
¿Has considerado un enfoque ligeramente diferente? En lugar de tener una representación separada de FooBar y BarBaz, considere un FooBarBaz similar a
message FooBarBaz {
optional Foo foo = 1;
optional Bar bar = 2;
optional Baz baz = 3;
}
Y luego, en el código de tu aplicación, podrías aprovecharla como:
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);
Alternativamente, puede aprovechar la codificación protobuf y serializar los mensajes. (el protobuf en sí no es en realidad serializado, obtendrías eso al llamar a uno de los métodos 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);
Esto supone que puede mover la mayoría de sus apis de conjuntos explícitos de campos y hacia mensajes comunes con campos opcionales para enviar solo lo que necesita. Es posible que desee envolver los bordes de su sistema para afirmar que los campos requeridos para un proceso en particular se configuran antes de intentar usar eso, pero puede llevar a menos repeticiones en otros lugares. El truco de concat de cadena también puede ser útil en los casos en los que está pasando por un sistema que realmente no le importa lo que hay en ellos.
En mi opinión, la mejor solución integral es un generador de código C ++ externo en un lenguaje de scripting. Tiene las siguientes ventajas:
Flexibilidad : le permite cambiar el código generado en cualquier momento. Esto es extremadamente bueno por varias razones:
- Solucione fácilmente los errores en todas las versiones soportadas antiguas.
- Use las nuevas funciones de C ++ si se muda a C ++ 11 o posterior en el futuro.
- Generar código para un idioma diferente. Esto es muy, muy útil (especialmente si su organización es grande y / o tiene muchos usuarios). Por ejemplo, podría generar una pequeña biblioteca de secuencias de comandos (por ejemplo, un módulo de Python) que se puede usar como una herramienta CLI para interactuar con el hardware. En mi experiencia, esto fue muy querido por los ingenieros de hardware.
- Genere código GUI (o descripciones de GUI, por ejemplo, en XML / JSON; o incluso una interfaz web): útil para las personas que usan el hardware final y los probadores.
- Generación de otro tipo de datos. Por ejemplo, diagramas, estadísticas, etc. O incluso las descripciones de protobuf.
Mantenimiento : será más fácil de mantener que en C ++. Incluso si está escrito en un lenguaje diferente, por lo general es más fácil aprender ese lenguaje que hacer que un nuevo desarrollador de C ++ se sumerja en la metaprogramación de la plantilla de C ++ (especialmente en C ++ 03).
Rendimiento : puede reducir fácilmente el tiempo de compilación del lado C ++ (ya que puede generar C ++ muy simple, incluso C simple). Por supuesto, el generador puede compensar esta ventaja. En su caso, es posible que esto no se aplique, ya que parece que no puede cambiar el código del cliente.
He utilizado ese enfoque en un par de proyectos / sistemas y resultó bastante bien. Especialmente las diferentes alternativas para usar el hardware (C ++ lib, Python lib, CLI, GUI ...) pueden ser muy apreciadas.
Nota al margen: si parte de la generación requiere analizar código C ++ ya existente (por ejemplo, los encabezados con tipos de datos deben ser serializados, como en el caso de OP con los tipos Serialized
); Entonces, una solución muy agradable es usar las herramientas de LLVM / clang para hacerlo.
En un proyecto en particular en el que trabajé, tuvimos que serializar docenas de tipos de C ++ automáticamente (que estaban sujetos a cambios en cualquier momento por parte de los usuarios). Logramos generar automáticamente el código para él simplemente usando los enlaces de Python de Clang e integrarlo en el proceso de compilación. Si bien los enlaces de Python no expusieron todos los detalles de AST (al menos en ese momento), fueron suficientes para generar el código de serialización requerido para todos nuestros tipos (que incluía clases con plantilla, contenedores, etc.).
Lo que quieres es algo parecido a una tupla pero no una tupla real. Suponiendo que todas tuple_like
clases de tuple_like
implementan tie()
que básicamente solo vincula a sus miembros, aquí está mi código hipotético:
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;
}
};
Mientras ambos lados implementen un lazo apropiado (), esto debería estar bien. Si las clases de origen o destino no están directamente bajo su control, recomiende definir una clase heredada que implemente tie () y use eso. Para fusionar varias clases, defina una clase auxiliar que implemente tie () en términos de sus miembros.
Me basaré en su solución propuesta, pero en su lugar use boost :: fusion :: tuples (suponiendo que esté permitido). Supongamos que sus tipos de datos son
struct Foo{};
struct Bar{};
struct Baz{};
struct Fbr{};
y tus datos son
struct some_data {
Foo foo;
Bar bar;
Baz baz;
Fbr fbr;
};
De los comentarios, entiendo que usted no tiene control sobre las clases SerialisedXYZ, pero sí tienen una interfaz determinada. Asumiré que algo como esto está lo suficientemente cerca (?):
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;
}
};
Ahora podemos reducir la plantilla y limitarla a un typedef por permutación de tipo de datos y una sobrecarga simple para cada miembro set_XXX de la clase SerializedXYZ, de la siguiente manera:
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);
}
//...
Lo bueno ahora es que ya no necesita especializar sus viajes de serialización. Lo siguiente hace uso de la función boost :: fusion :: fold, que supongo que está bien usar en su proyecto:
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;
}
};
Y aquí hay algunos ejemplos de cómo funciona. Tenga en cuenta que si alguien intenta vincular un miembro de datos de some_data que no cumple con la interfaz SerializedXYZ, el compilador le informará al respecto:
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);
}
Codifique here
EDITAR:
Desafortunadamente, esta solución no aborda el problema # 1 del OP. Para remediar esto, podemos definir una serie de etiquetas, una para cada uno de sus miembros de datos y seguir un enfoque similar. Aquí están las etiquetas, junto con las funciones serialized_set
modificadas:
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);
}
La placa de repetición se limita nuevamente a un serialized_set
por miembro de datos y se escala linealmente, de manera similar a mi respuesta anterior. Aquí está el serialization_traits modificado:
// 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;
}
};
Y así es como funciona:
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())));
}
Código actualizado here
Si su repetitivo es realmente un montón de estructuras de datos simples y triviales con operadores de comparación, probablemente podría salirse con algunas macros.
#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); } /
};
El uso se vería como:
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;
}
};
Necesitaría N macros, donde N es el número de permutaciones de conteos de argumentos que tiene, pero ese sería un costo inicial único.
Alternativamente, podría aprovechar las tuplas para hacer una gran parte del trabajo pesado como usted sugirió. Aquí he creado una plantilla "NamedTuple" para nombrar a los captadores de la tupla.
#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)
Uso:
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;
}
};