c++ c++11 inheritance

c++ - ¿Cómo elimino este olor de código relacionado con la herencia?



c++11 inheritance (8)

Necesito implementar muchas clases derivadas con diferentes datos de miembros const. El procesamiento de datos debe manejarse en la clase base, pero no puedo encontrar una manera elegante de acceder a los datos derivados. El código de abajo está funcionando, pero realmente no me gusta.

El código debe ejecutarse en un entorno pequeño e integrado, por lo que el uso extensivo de bibliotecas dinámicas o de lujo como Boost no es una opción.

class Base { public: struct SomeInfo { const char *name; const f32_t value; }; void iterateInfo() { // I would love to just write // for(const auto& info : c_myInfo) {...} u8_t len = 0; const auto *returnedInfo = getDerivedInfo(len); for (int i = 0; i < len; i++) { DPRINTF("Name: %s - Value: %f /n", returnedInfo[i].name, returnedInfo[i].value); } } virtual const SomeInfo* getDerivedInfo(u8_t &length) = 0; }; class DerivedA : public Base { public: const SomeInfo c_myInfo[2] { {"NameA1", 1.1f}, {"NameA2", 1.2f} }; virtual const SomeInfo* getDerivedInfo(u8_t &length) override { // Duplicated code in every derived implementation.... length = sizeof(c_myInfo) / sizeof(c_myInfo[0]); return c_myInfo; } }; class DerivedB : public Base { public: const SomeInfo c_myInfo[3] { {"NameB1", 2.1f}, {"NameB2", 2.2f}, {"NameB2", 2.3f} }; virtual const SomeInfo *getDerivedInfo(u8_t &length) override { // Duplicated code in every derived implementation.... length = sizeof(c_myInfo) / sizeof(c_myInfo[0]); return c_myInfo; } }; DerivedA instanceA; DerivedB instanceB; instanceA.iterateInfo(); instanceB.iterateInfo();


Bien, entonces simplifiquemos todas las complicaciones innecesarias :)

Su código realmente se reduce a lo siguiente:

SomeInfo.h

struct SomeInfo { const char *name; const f32_t value; }; void processData(const SomeInfo* c_myInfo, u8_t len);

SomeInfo.cpp

#include "SomeInfo.h" void processData(const SomeInfo* c_myInfo, u8_t len) { for (u8_t i = 0; i < len; i++) { DPRINTF("Name: %s - Value: %f /n", c_myInfo[i].name, c_myInfo[i].value); } }

datos.h

#include "SomeInfo.h" struct A { const SomeInfo info[2] { {"NameA1", 1.1f}, {"NameA2", 1.2f} }; static const u8_t len = 2; }; struct B { const SomeInfo info[3] { {"NameB1", 2.1f}, {"NameB2", 2.2f}, {"NameB2", 2.3f} }; static const u8_t len = 3; };

main.cpp

#include "data.h" int main() { A a; B b; processData(a.info, A::len); processData(b.info, B::len); }


Comience con un tipo de vocabulario:

template<class T> struct span { T* b = nullptr; T* e = nullptr; span( T* s, T* f ):b(s), e(f) {} span( T* s, size_t l ):span(s, s+l) {} template<size_t N> span( T(&arr)[N] ):span(arr, N) {} T* begin() const { return b; } T* end() const { return e; } size_t size() const { return end()-begin(); } bool empty() const { return size()==0; } T& front() const { return *begin(); } T& back() const { return *(end()-1); } }; // This is just here for the other array ctor: template<class T> struct span<T const> { T const* b = nullptr; T const* e = nullptr; span( T const* s, T const* f ):b(s), e(f) {} span( T const* s, size_t l ):span(s, s+l) {} template<size_t N> span( T const(&arr)[N] ):span(arr, N) {} template<size_t N> span( T(&arr)[N] ):span(arr, N) {} T const* begin() const { return b; } T const* end() const { return e; } size_t size() const { return end()-begin(); } bool empty() const { return size()==0; } T const& front() const { return *begin(); } T const& back() const { return *(end()-1); } };

Ahora podemos hablar de un span<char> :

class Base { public: void iterateInfo() { for(const auto& info : c_mySpan) { DPRINTF("Name: %s - Value: %f /n", info.name, info.value); } } private: span<const char> c_mySpan; Base( span<const char> s ):c_mySpan(s) {} Base(Base const&)=delete; // probably unsafe };

Ahora su derivado se ve como:

class DerivedA : public Base { public: const SomeInfo c_myInfo[2] { {"NameA1", 1.1f}, {"NameA2", 1.2f} }; DerivedA() : Base(c_myInfo) {} };

Esto tiene una sobrecarga de dos punteros por Base . Un vtable usa un puntero, hace que su tipo sea abstracto, agrega direccionamiento indirecto y agrega un vtable global por tipo Derived .

Ahora, en teoría, podría obtener la sobrecarga de esto a la longitud de la matriz, y suponer que los datos de la matriz comienzan justo después de la Base , pero eso es frágil, no portátil y solo es útil si está desesperado.

Si bien es posible que desconfíe de las plantillas en el código incrustado (como debería ser de cualquier tipo de generación de código, la generación de código significa que puede generar más de O (1) binario desde el código O (1)). El tipo de vocabulario de span es compacto y no debería estar en línea si la configuración del compilador es razonablemente agresiva.


No necesitas ningún virtual o plantillas aquí. Simplemente agregue un puntero SomeInfo* y su longitud a la Base , y proporcione un constructor protegido para inicializarlos (y como no hay un constructor predeterminado, no será posible olvidar iniciarlos).

El constructor que se está protegiendo no es un requisito difícil, pero dado que la Base ya no es una clase base abstracta, hacer que el constructor protegido evite que la Base sea ​​instanciada.

class Base { public: struct SomeInfo { const char *name; const f32_t value; }; void iterateInfo() { for (int i = 0; i < c_info_len; ++i) { DPRINTF("Name: %s - Value: %f /n", c_info[i].name, c_info[i].value); } } protected: explicit Base(const SomeInfo* info, int len) noexcept : c_info(info) , c_info_len(len) { } private: const SomeInfo* c_info; int c_info_len; }; class DerivedA : public Base { public: DerivedA() noexcept : Base(c_myInfo, sizeof(c_myInfo) / sizeof(c_myInfo[0])) { } private: const SomeInfo c_myInfo[2] { {"NameA1", 1.1f}, {"NameA2", 1.2f} }; }; class DerivedB : public Base { public: DerivedB() noexcept : Base(c_myInfo, sizeof(c_myInfo) / sizeof(c_myInfo[0])) { } private: const SomeInfo c_myInfo[3] { {"NameB1", 2.1f}, {"NameB2", 2.2f}, {"NameB2", 2.3f} }; };

Por supuesto, puede usar una clase de adaptador / envoltura pequeña y sin sobrecarga en lugar de los miembros c_info y c_info_len para proporcionar un acceso más agradable y seguro (como el soporte begin() y end() ), pero eso está fuera del alcance de esta respuesta. .

Como señaló Peter Cordes, un problema con este enfoque es que los objetos derivados ahora son más grandes por el tamaño de un puntero más el tamaño de un int si su código final aún usa virtuales (funciones virtuales que no ha mostrado en su publicación. ) Si ya no hay virtuales, el tamaño del objeto solo aumentará en un int . Dijiste que estás en un pequeño entorno integrado, por lo que si muchos de estos objetos van a estar vivos al mismo tiempo, entonces esto podría ser algo de qué preocuparse.

Peter también señaló que, dado que las matrices c_myInfo son const y usan inicializadores constantes, también puede hacer que sean static . Esto reducirá el tamaño de cada objeto derivado por el tamaño de la matriz.


Podrías hacer de Base una plantilla y tomar la longitud de tu matriz const. Algo como esto:

template<std::size_t Length> class Base { public: struct SomeInfo { const char *name; const float value; }; const SomeInfo c_myInfo[Length]; void iterateInfo() { //I would love to just write for(const auto& info : c_myInfo) { // work with info } } };

Y luego inicialice la matriz en consecuencia de cada clase base:

class DerivedA : public Base<2> { public: DerivedA() : Base<2>{ SomeInfo{"NameA1", 1.1f}, {"NameA2", 1.2f} } {} }; class DerivedB : public Base<3> { public: DerivedB() : Base<3>{ SomeInfo{"NameB1", 2.1f}, {"NameB2", 2.2f}, {"NameB2", 2.3f} } {} };

Y luego use como lo haría normalmente. Este método elimina el polimorfismo y no utiliza la asignación de almacenamiento dinámico (por ejemplo, no std::vector ), tal como solicitó el usuario SirNobbyNobbs .


Puede mover sus datos a una matriz bidimensional fuera de las clases y hacer que cada clase devuelva un índice que contenga datos relevantes.

struct SomeInfo { const char *name; const f32_t value; }; const vector<vector<SomeInfo>> masterStore{ {{"NameA1", 1.1f}, {"NameA2", 1.2f}}, {{"NameB1", 2.1f}, {"NameB2", 2.2f}, {"NameB2", 2.3f}} }; class Base { public: void iterateInfo() { // I would love to just write // for(const auto& info : c_myInfo) {...} u8_t len = 0; auto index(getIndex()); for(const auto& data : masterStore[index]) { DPRINTF("Name: %s - Value: %f /n", data.name, data.value); } } virtual int getIndex() = 0; }; class DerivedA : public Base { public: int getIndex() override { return 0; } }; class DerivedB : public Base { public: int getIndex() override { return 1; } }; DerivedA instanceA; DerivedB instanceB; instanceA.iterateInfo(); instanceB.iterateInfo();


Puedes usar CRTP:

template<class Derived> class impl_getDerivedInfo :public Base { virtual const SomeInfo *getDerivedInfo(u8_t &length) override { //Duplicated code in every derived implementation.... auto& self = static_cast<Derived&>(*this); length = sizeof(self.c_myInfo) / sizeof(self.c_myInfo[0]); return self.c_myInfo; } }; class DerivedA : public impl_getDerivedInfo<DerivedA> { public: const SomeInfo c_myInfo[2] { {"NameA1", 1.1f}, {"NameA2", 1.2f} }; }; class DerivedB : public impl_getDerivedInfo<DerivedB> { public: const SomeInfo c_myInfo[3] { {"NameB1", 2.1f}, {"NameB2", 2.2f}, {"NameB2", 2.3f} }; };


Simplemente haga que la función virtual devuelva una referencia a los datos directamente (debe cambiar a vector entonces; no es posible con matrices o tipos de matrices de estilo C con diferentes tamaños):

virtual const std::vector<SomeInfo>& getDerivedInfo() = 0;

o si los punteros son la única opción factible, como un rango de punteros (si fuera posible, se preferirían los iteradores / adaptadores de rango, más sobre eso):

virtual std::pair<SomeInfo*, SomeInfo*> getDerivedInfo() = 0;

Para hacer que este último método funcione con un rango basado en bucle for : una forma es hacer un tipo pequeño de ''vista de rango'' que tenga las funciones begin()/end() - un par esencial con begin()/end()

Ejemplo:

template<class T> struct ptr_range { std::pair<T*, T*> range_; auto begin(){return range_.begin();} auto end(){return range_.end();} };

Luego constrúyelo con:

virtual ptr_range<SomeInfo> getDerivedInfo() override { return {std::begin(c_myInfo), std::end(c_myInfo)}; }

Es fácil hacerlo sin plantilla si no se desea una plantilla.


Una forma con C ++ 17 sería devolver un objeto de "vista" que represente su lista de contenido. Esto puede ser utilizado en un C ++ 11 for declaración. Podría escribir una función base que convierte start+len en una vista, por lo que no necesita agregar al método virtual cruft.

No es tan difícil crear un objeto de vista que sea compatible con C ++ 11 para la declaración. Alternativamente, podría considerar el uso de C ++ 98 for_each templates que pueden tomar un iterador de inicio y finalización: su iterador de start es start ; El iterador final es start+len .