resueltos relaciones programacion poo orientada objetos herencia entre ejercicios ejemplos codigo clases c++ inheritance architecture embedded real-time

relaciones - programacion orientada a objetos c++ ejemplos



Configuración eficiente de la jerarquía de clases en tiempo de compilación (6)

Esta pregunta es específicamente sobre la arquitectura C ++ en sistemas incrustados y duros en tiempo real. Esto implica que grandes partes de las estructuras de datos, así como el flujo de programa exacto se dan en tiempo de compilación, el rendimiento es importante y se puede incluir una gran cantidad de código. Las soluciones preferiblemente solo usan C ++ 03, pero las entradas de C ++ 11 también son bienvenidas.

Estoy buscando patrones de diseño establecidos y soluciones para el problema arquitectónico donde la misma base de código debe reutilizarse para varios productos estrechamente relacionados, mientras que algunas partes (por ejemplo, la abstracción del hardware) serán necesariamente diferentes.

Es probable que termine con una estructura jerárquica de módulos encapsulados en clases que podrían verse así de alguna manera, asumiendo 4 capas:

Product A Product B Toplevel_A Toplevel_B (different for A and B, but with common parts) Middle_generic Middle_generic (same for A and B) Sub_generic Sub_generic (same for A and B) Hardware_A Hardware_B (different for A and B)

Aquí, algunas clases heredan de una clase base común (por ejemplo, Toplevel_A de Toplevel_base ) mientras que otras no necesitan ser especializadas en absoluto (por ejemplo, Middle_generic ).

Actualmente puedo pensar en los siguientes enfoques:

  • (A) : si se tratara de una aplicación de escritorio normal, utilizaría la herencia virtual y crearía las instancias en tiempo de ejecución, usando, por ejemplo, una fábrica abstracta.

    Drawback : Sin embargo, las clases *_B nunca se usarán en el producto A y, por lo tanto, la eliminación de referencias de todas las llamadas a funciones virtuales y los miembros no vinculados a una dirección en tiempo de ejecución generarán una sobrecarga.

  • (B) Uso de especialización de plantilla como mecanismo de herencia (por ejemplo, CRTP )

    template<class Derived> class Toplevel { /* generic stuff ... */ }; class Toplevel_A : public Toplevel<Toplevel_A> { /* specific stuff ... */ };

    Drawback : difícil de entender.

  • (C) : use diferentes conjuntos de archivos coincidentes y permita que los scripts de compilación incluyan el correcto

    // common/toplevel_base.h class Toplevel_base { /* ... */ }; // product_A/toplevel.h class Toplevel : Toplevel_base { /* ... */ }; // product_B/toplevel.h class Toplevel : Toplevel_base { /* ... */ }; // build_script.A compiler -Icommon -Iproduct_A

    Drawback : Confuso, difícil de mantener y probar.

  • (D) : un gran archivo typedef (o #define)

    //typedef_A.h typedef Toplevel_A Toplevel_to_be_used; typedef Hardware_A Hardware_to_be_used; // etc. // sub_generic.h class sub_generic { Hardware_to_be_used the_hardware; // etc. };

    Drawback : un archivo para ser incluido en todas partes y la necesidad de otro mecanismo para cambiar entre diferentes configuraciones.

  • (E) : una configuración similar, "basada en políticas" , por ejemplo

    template <class Policy> class Toplevel { Middle_generic<Policy> the_middle; // ... }; // ... template <class Policy> class Sub_generic { class Policy::Hardware_to_be_used the_hardware; // ... }; // used as class Policy_A { typedef Hardware_A Hardware_to_be_used; }; Toplevel<Policy_A> the_toplevel;

    Drawback : todo es una plantilla ahora; es necesario volver a compilar una gran cantidad de código cada vez.

  • (F) : compilador de interruptor y preprocesador

    // sub_generic.h class Sub_generic { #if PRODUCT_IS_A Hardware_A _hardware; #endif #if PRODUCT_IS_B Hardware_B _hardware; #endif };

    Drawback : Brrr ..., solo si todo lo demás falla.

¿Hay algún (otro) patrón de diseño establecido o una mejor solución a este problema, de forma que el compilador pueda asignar de forma estática tantos objetos como sea posible y en línea grandes partes del código, sabiendo qué producto se está creando y qué clases se van a ¿ser usado?


Entiendo que tienes dos requisitos importantes:

  1. Los tipos de datos son conocidos en tiempo de compilación
  2. El flujo de programa es conocido en tiempo de compilación

El CRTP realmente no resolvería el problema que está tratando de resolver, ya que le permitiría a HardwareLayer llamar a los métodos Sub_generic , Middle_generic o TopLevel y no creo que sea lo que está buscando.

Ambos requisitos pueden cumplirse utilizando el patrón Rasgo ( otra referencia ). Aquí hay un ejemplo que demuestra que se cumplen ambos requisitos. En primer lugar, definimos shells vacíos que representan dos Hardwares que quizás deseemos admitir.

class Hardware_A {}; class Hardware_B {};

Entonces consideremos una clase que describa un caso general que corresponde a Hardware_A .

template <typename Hardware> class HardwareLayer { public: typedef long int64_t; static int64_t getCPUSerialNumber() {return 0;} };

Ahora veamos una especialización para Hardware_B:

template <> class HardwareLayer<Hardware_B> { public: typedef int int64_t; static int64_t getCPUSerialNumber() {return 1;} };

Ahora, aquí hay un ejemplo de uso dentro de la capa Sub_generic:

template <typename Hardware> class Sub_generic { public: typedef HardwareLayer<Hardware> HwLayer; typedef typename HwLayer::int64_t int64_t; int64_t doSomething() {return HwLayer::getCPUSerialNumber();} };

Y finalmente, un main corto que ejecuta ambas rutas de código y usa ambos tipos de datos:

int main(int argc, const char * argv[]) { std::cout << "Hardware_A : " << Sub_generic<Hardware_A>().doSomething() << std::endl; std::cout << "Hardware_B : " << Sub_generic<Hardware_B>().doSomething() << std::endl; }

Ahora, si su HardwareLayer necesita mantener el estado, aquí hay otra forma de implementar las clases de capa HardLayer y Sub_generic.

template <typename Hardware> class HardwareLayer { public: typedef long hwint64_t; hwint64_t getCPUSerialNumber() {return mySerial;} private: hwint64_t mySerial = 0; }; template <> class HardwareLayer<Hardware_B> { public: typedef int hwint64_t; hwint64_t getCPUSerialNumber() {return mySerial;} private: hwint64_t mySerial = 1; }; template <typename Hardware> class Sub_generic : public HardwareLayer<Hardware> { public: typedef HardwareLayer<Hardware> HwLayer; typedef typename HwLayer::hwint64_t hwint64_t; hwint64_t doSomething() {return HwLayer::getCPUSerialNumber();} };

Y aquí hay una última variante donde solo cambia la implementación Sub_generic:

template <typename Hardware> class Sub_generic { public: typedef HardwareLayer<Hardware> HwLayer; typedef typename HwLayer::hwint64_t hwint64_t; hwint64_t doSomething() {return hw.getCPUSerialNumber();} private: HwLayer hw; };


Yo buscaría A. Hasta que se PROBABLE que esto no sea lo suficientemente bueno, tome las mismas decisiones que para el escritorio (bueno, por supuesto, asignar varios kilobytes en la pila, o usar variables globales que son muchos megabytes grandes puede ser " Obvio "que no va a funcionar". Sí, hay ALGUNOS gastos indirectos en las funciones virtuales de llamada, pero yo preferiría la solución C ++ más obvia y natural PRIMERO, luego rediseñarla si no es "lo suficientemente buena" (obviamente, intentar determinar el rendimiento y eso desde el principio, y usar herramientas como un generador de perfiles de muestreo para determinar dónde está gastando su tiempo, en lugar de "adivinar": los humanos son conjeturas bastante pobres).

Luego pasaría a la opción B si se demuestra que A no funciona. Esto no es del todo obvio, pero es, aproximadamente, cómo LLVM / Clang resuelve este problema para las combinaciones de hardware y sistema operativo, ver: https://github.com/llvm-mirror/clang/blob/master/lib/Basic /Targets.cpp


En una línea de pensamiento similar a F, podría tener un diseño de directorio como este:

Hardware/ common/inc/hardware.h hardware1/src/hardware.cpp hardware2/src/hardware.cpp

Simplifique la interfaz para solo asumir que existe un solo hardware:

// sub_generic.h class Sub_generic { Hardware _hardware; };

Y luego solo compile la carpeta que contiene los archivos .cpp para el hardware de esa plataforma.

Los beneficios de este enfoque son:

  • Es simple entender qué está sucediendo y agregar un hardware3
  • hardware.h todavía sirve como su API
  • Quita la abstracción del compilador (por cuestiones de velocidad)
  • El compilador 1 no necesita compilar hardware2.cpp o hardware3.cpp que pueden contener cosas que el compilador 1 no puede hacer (como el ensamblaje en línea o alguna otra cosa específica del compilador 2)
  • hardware3 podría ser mucho más complicado por alguna razón que aún no hayas considerado ... por lo que darle una estructura de directorio completa lo encapsula.

Primero me gustaría señalar que básicamente respondiste tu propia pregunta en la pregunta :-)

A continuación, me gustaría señalar que en C ++

el flujo de programa exacto se da en tiempo de compilación, el rendimiento es importante y se puede incluir una gran cantidad de código

se llama plantillas . Los otros enfoques que aprovechan las funciones de idioma en oposición a las funciones de compilación del sistema servirán solo como una forma lógica de estructurar el código en su proyecto en beneficio de los desarrolladores.

Además, como se señala en otras respuestas, C es más común para los sistemas duros en tiempo real que C ++ , y en C es habitual confiar en MACROS para realizar este tipo de optimización en tiempo de compilación.

Finalmente, ha señalado en su solución B anterior que la especialización de plantillas es difícil de entender. Yo diría que esto depende de cómo lo hagas y también de la experiencia que tu equipo tenga en C ++ / templates. Encuentro que muchos proyectos "montados en plantilla" son extremadamente difíciles de leer y los mensajes de error que producen son impíos en el mejor de los casos, pero aún logro hacer un uso efectivo de las plantillas en mis propios proyectos porque respeto el principio KISS mientras lo hago.

Así que mi respuesta es ir con B o dejar C ++ para C


Voy a suponer que estas clases solo necesitan crearse una sola vez, y que sus instancias persisten durante todo el tiempo de ejecución del programa.

En este caso, recomendaría usar el patrón Object Factory ya que la fábrica solo se ejecutará una vez para crear la clase. A partir de ese punto, las clases especializadas son todas de tipo conocido.


Dado que se trata de un sistema embebido duro en tiempo real, por lo general, debería elegir un tipo de solución C no c ++.

Con compiladores modernos, diría que la sobrecarga de c ++ no es tan buena, por lo que no es una cuestión de rendimiento, pero los sistemas integrados tienden a preferir c en lugar de c ++. Lo que intentas construir se parecería a una biblioteca de controladores de dispositivo clásica (como la de los chips ftdi).

El enfoque allí sería (ya que está escrito en C) algo similar a tu F, pero sin opciones de tiempo de compilación, podrías especializar el código, en tiempo de ejecución, basado en algo como PID, VID, SN, etc.

Ahora bien, si tiene que usar c ++ para esto, las plantillas probablemente sean su última opción (la legibilidad de los códigos suele ser más alta que cualquier otra ventaja que las plantillas aporten). Entonces, probablemente buscaría algo similar a A: un esquema de herencia de clase básica, pero no se requiere un patrón de diseño particularmente sofisticado.

Espero que esto ayude...