una resueltos que programacion orientada objetos miembros ejercicios ejemplos codigo clases clase c++ design-patterns templates abstract-class virtual-functions

c++ - resueltos - ¿Plantilla o clase base abstracta?



programacion orientada a objetos c++ ejemplos (9)

Si quiero hacer una clase adaptable y hacer posible seleccionar diferentes algoritmos desde el exterior, ¿cuál es la mejor implementación en C ++?

Veo principalmente dos posibilidades:

  • Usa una clase base abstracta y pasa el objeto concreto en
  • Usa una plantilla

Aquí hay un pequeño ejemplo, implementado en las diversas versiones:

Versión 1: clase base abstracta

class Brake { public: virtual void stopCar() = 0; }; class BrakeWithABS : public Brake { public: void stopCar() { ... } }; class Car { Brake* _brake; public: Car(Brake* brake) : _brake(brake) { brake->stopCar(); } };

Versión 2a: Plantilla

template<class Brake> class Car { Brake brake; public: Car(){ brake.stopCar(); } };

Versión 2b: Plantilla y herencia privada

template<class Brake> class Car : private Brake { using Brake::stopCar; public: Car(){ stopCar(); } };

Procedente de Java, estoy naturalmente inclinado a usar siempre la versión 1, pero las versiones de las plantillas parecen ser preferidas a menudo, por ejemplo, en el código STL. Si eso es cierto, ¿es solo por la eficiencia de la memoria, etc. (sin herencia, sin llamadas a funciones virtuales)?

Me doy cuenta de que no hay una gran diferencia entre la versión 2a y la 2b, consulte las preguntas frecuentes de C ++ .

¿Puedes comentar sobre estas posibilidades?


Esto depende de tus objetivos. Puedes usar la versión 1 si

  • Intenta reemplazar los frenos de un automóvil (en tiempo de ejecución)
  • Intentar pasar Auto a funciones que no son de plantilla

En general, preferiría la versión 1 usando el polimorfismo de tiempo de ejecución, porque aún es flexible y permite que el automóvil siga teniendo el mismo tipo: Car<Opel> es otro tipo de Car<Nissan> . Si sus objetivos son un gran rendimiento mientras usa los frenos con frecuencia, le recomiendo usar el enfoque de plantilla. Por cierto, esto se llama diseño basado en políticas. Usted proporciona una política de freno . Ejemplo porque dijiste que programabas en Java, posiblemente aún no tienes mucha experiencia con C ++. Una forma de hacerlo:

template<typename Accelerator, typename Brakes> class Car { Accelerator accelerator; Brakes brakes; public: void brake() { brakes.brake(); } }

Si tiene muchas políticas, puede agruparlas en su propia estructura y pasarlas, por ejemplo, como una SpeedConfiguration Accelerator , Brakes y algunas más. En mis proyectos trato de mantener una gran cantidad de códigos libres de plantillas, lo que les permite ser compilados una vez en sus propios archivos de objetos, sin necesidad de tener su código en los encabezados, pero aún así permitir el polimorfismo (a través de funciones virtuales). Por ejemplo, es posible que desee conservar los datos y funciones comunes que el código sin plantilla probablemente invocará en muchas ocasiones en una clase base:

class VehicleBase { protected: std::string model; std::string manufacturer; // ... public: ~VehicleBase() { } virtual bool checkHealth() = 0; }; template<typename Accelerator, typename Breaks> class Car : public VehicleBase { Accelerator accelerator; Breaks breaks; // ... virtual bool checkHealth() { ... } };

Dicho sea de paso, ese es también el enfoque que usan los flujos de C ++: std::ios_base contiene indicadores y cosas que no dependen del tipo de caracteres o rasgos como modo abierto, banderas de formato y std::basic_ios , mientras que std::basic_ios es una plantilla de clase que hereda eso. Esto también reduce la saturación del código al compartir el código que es común a todas las instancias de una plantilla de clase.

¿Herencia privada?

La herencia privada debe evitarse en general. Rara vez es útil y la contención es una mejor idea en la mayoría de los casos. Caso común donde lo contrario es cierto cuando el tamaño es realmente crucial (clase de cadena basada en políticas, por ejemplo): la optimización de clase base vacía puede aplicarse cuando se deriva de una clase de política vacía (que solo contiene funciones).

Leer Usos y abusos de herencia por Herb Sutter.


La clase base abstracta tiene una sobrecarga de llamadas virtuales, pero tiene la ventaja de que todas las clases derivadas son realmente clases base. No es así cuando usa plantillas: Car <Brake> y Car <BrakeWithABS> no están relacionadas entre sí y tendrá que realizar un dynamic_cast y buscar null o tener plantillas para todo el código que trata con Car.


La mayoría de tu pregunta ya ha sido respondida, pero quería profundizar en este tema:

Procedente de Java, estoy naturalmente inclinado a usar siempre la versión 1, pero las versiones de las plantillas parecen ser preferidas a menudo, por ejemplo, en el código STL. Si eso es cierto, ¿es solo por la eficiencia de la memoria, etc. (sin herencia, sin llamadas a funciones virtuales)?

Eso es parte de eso Pero otro factor es la seguridad del tipo agregado. Cuando trata un BrakeWithABS como un Brake , pierde información de tipo. Ya no sabes que el objeto es realmente un BrakeWithABS . Si se trata de un parámetro de plantilla, tiene el tipo exacto disponible, que en algunos casos puede permitir al compilador realizar una mejor comprobación de tipos. O puede ser útil para garantizar que se llame a la sobrecarga correcta de una función. (si stopCar() pasa el objeto Brake a una segunda función, que puede tener una sobrecarga por separado para BrakeWithABS , que no se BrakeWithABS si hubiera usado herencia, y su BrakeWithABS hubiera lanzado a un Brake .

Otro factor es que permite más flexibilidad. ¿Por qué todas las implementaciones de Brake tienen que heredar de la misma clase base? ¿La clase base realmente tiene algo que traer a la mesa? Si escribo una clase que expone las funciones esperadas de los miembros, ¿no es lo suficientemente bueno para actuar como un freno? A menudo, el uso explícito de interfaces o clases base abstractas restringe tu código más de lo necesario.

(Tenga en cuenta que no estoy diciendo que las plantillas siempre sean la solución preferida. Hay otras preocupaciones que podrían afectar esto, desde la velocidad de compilación hasta "lo que los programadores de mi equipo están familiarizados" o simplemente "lo que prefiero". Y a veces , necesita polimorfismo en tiempo de ejecución, en cuyo caso la solución de plantilla simplemente no es posible)


La regla de oro es:

1) Si la elección del tipo concreto se realiza en tiempo de compilación, prefiera una plantilla. Será más seguro (errores de tiempo de compilación frente a errores de tiempo de ejecución) y probablemente mejor optimizado. 2) Si la elección se realiza en tiempo de ejecución (es decir, como resultado de una acción del usuario) realmente no hay otra opción: usar herencia y funciones virtuales.


Las plantillas son una forma de dejar que una clase use una variable de la que realmente no le importa el tipo. La herencia es una forma de definir qué clase se basa en sus atributos. Es la pregunta "is-a" versus "has-a" .


Otras opciones:

  1. Use el patrón de visitante (deje que el código externo funcione en su clase).
  2. Externalice una parte de su clase, por ejemplo, a través de iteradores, para que el código genérico basado en iteradores pueda funcionar en ellos. Esto funciona mejor si su objeto es un contenedor de otros objetos.
  3. Ver también el Patrón de estrategia (hay ejemplos de c ++ en el interior)

Personalmente, siempre preferiría utilizar interfaces sobre plantillas por varias razones:

  1. Plantillas Los errores de compilación y enlace a veces son crípticos
  2. Es difícil depurar un código basado en plantillas (al menos en el estudio visual IDE)
  3. Las plantillas pueden hacer que tus binarios sean más grandes.
  4. Las plantillas requieren que coloque todo su código en el archivo de encabezado, lo que hace que la clase de plantilla sea un poco más difícil de entender.
  5. Las plantillas son difíciles de mantener por los programadores novatos.

Solo uso plantillas cuando las tablas virtuales crean algún tipo de sobrecarga.

Por supuesto, esta es solo mi opinión propia.


Use la interfaz si supone que admite diferentes clases de interrupción y su jerarquía a la vez.

Car( new Brake() ) Car( new BrakeABC() ) Car( new CoolBrake() )

Y no conoce esta información en tiempo de compilación.

Si sabes qué Break vas a utilizar, 2b es la opción correcta para que especifiques diferentes clases de coches. En este caso, Brake será su "Estrategia" de automóvil y podrá establecer la predeterminada.

No usaría 2a. En su lugar, puede agregar métodos estáticos para romperlos y llamarlos sin instancia.


esta respuesta es más o menos correcta. Cuando desee algo parametrizado en tiempo de compilación, debería preferir las plantillas. Cuando desee algo parametrizado en el tiempo de ejecución, debería preferir que se anulen las funciones virtuales.

Sin embargo , el uso de plantillas no le impide hacer ambas cosas (lo que hace que la versión de la plantilla sea más flexible):

struct Brake { virtual void stopCar() = 0; }; struct BrakeChooser { BrakeChooser(Brake *brake) : brake(brake) {} void stopCar() { brake->stopCar(); } Brake *brake; }; template<class Brake> struct Car { Car(Brake brake = Brake()) : brake(brake) {} void slamTheBrakePedal() { brake.stopCar(); } Brake brake; }; // instantiation Car<BrakeChooser> car(BrakeChooser(new AntiLockBrakes()));

Dicho esto, probablemente NO use plantillas para esto ... Pero es realmente solo gusto personal.