Pregunta

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

Veo principalmente dos posibilidades:

  • Utilice una clase base abstracta y pase un objeto concreto
  • Usa una plantilla

A continuación se muestra un pequeño ejemplo, implementado en las distintas 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(); }
};

Al venir de Java, me inclino naturalmente a usar siempre la versión 1, pero las versiones de las plantillas parecen preferirse a menudo, p.en 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 2b, consulte Preguntas frecuentes sobre C++.

¿Puedes comentar sobre estas posibilidades?

¿Fue útil?

Solución

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

  • Tiene la intención de reemplazar los frenos de un automóvil (en tiempo de ejecución)
  • Tiene la intención de pasar Car a funciones que no son de plantilla

Generalmente preferiría que la versión 1 use el polimorfismo de tiempo de ejecución, porque aún es flexible y permite que el Car siga teniendo el mismo tipo: Car<Opel> es otro tipo que Car<Nissan>.Si sus objetivos son un gran rendimiento mientras usa los frenos con frecuencia, le recomiendo que utilice el enfoque basado en plantilla.Por cierto, esto se llama diseño basado en políticas.usted proporciona un política de frenos.Ejemplo porque dijiste que programaste en Java, posiblemente aún no tengas 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 pasarla, por ejemplo, como SpeedConfiguration coleccionando Accelerator, Brakes y algo más.En mis proyectos trato de mantener una gran cantidad de código libre de plantillas, lo que les permite compilarse una vez en sus propios archivos objeto, sin necesidad de su código en los encabezados, pero aún permitiendo el polimorfismo (a través de funciones virtuales).Por ejemplo, es posible que desee conservar datos y funciones comunes que el código que no es de plantilla probablemente llamará 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() { ... }
};

Por cierto, ese es también el enfoque que utilizan los flujos de C++: std::ios_base contiene banderas y cosas que no dependen del tipo de carácter o rasgos como modo abierto, banderas de formato y esas cosas, mientras std::basic_ios entonces hay una plantilla de clase que la hereda.Esto también reduce la sobrecarga de código al compartir el código que es común a todas las instancias de una plantilla de clase.

¿Herencia privada?

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

Leer Usos y abusos de la Herencia por Herb Sutter.

Otros consejos

La regla general es:

1) Si la elección del tipo de hormigón 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 esté mejor optimizado. 2) Si la elección se realiza en tiempo de ejecución (es decir, como resultado de la acción de un usuario), realmente no hay elección: utilizar la herencia y las funciones virtuales.

Otras opciones:

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

Las plantillas son una forma de permitir que una clase use una variable de la que realmente no te importa el tipo. La herencia es una forma de definir en qué se basa una clase en función de sus atributos. Es el & Quot; is-a & Quot; versus " has-a " pregunta.

La mayoría de sus preguntas ya han sido respondidas, pero quería dar más detalles sobre esto:

  

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

Eso es parte de eso. Pero otro factor es el tipo de seguridad adicional. Cuando trata un BrakeWithABS como un Brake, pierde información de tipo. Ya no sabes que el objeto es en realidad un stopCar(). Si es un parámetro de plantilla, tiene el tipo exacto disponible, que en algunos casos puede permitir que el compilador realice una mejor verificación de tipo. O puede ser útil para garantizar que se invoque la sobrecarga correcta de una función. (si <=> pasa el objeto Brake a una segunda función, que puede tener una sobrecarga separada para <=>, no se invocará si hubiera utilizado la herencia, y su <=> se hubiera convertido en un <= >.

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

(Tenga en cuenta que no digo que las plantillas siempre sean la solución preferida. Hay otras preocupaciones que pueden afectar esto, que van desde la velocidad de compilación hasta & "; ¡con qué están familiarizados los programadores de mi equipo &" ; o simplemente " lo que prefiero " ;. Y a veces, necesitas el polimorfismo de tiempo de ejecución, en cuyo caso la solución de plantilla simplemente no es posible)

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

Sin embargo , el uso de plantillas no le impide hacer ambas cosas (haciendo 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 usaría plantillas para esto ... Pero en realidad es solo un gusto personal.

La clase base abstracta tiene sobrecarga de llamadas virtuales pero tiene la ventaja de que todas las clases derivadas son realmente clases base. No es así cuando usa plantillas & # 8211; Car & Lt; Brake & Gt; y Car < BrakeWithABS > no están relacionados entre sí y tendrá que ya sea dynamic_cast y verificar nulo o tener plantillas para todo el código que trata con Car.

Utilice la interfaz si supone admitir diferentes clases de Break 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 sabe qué descanso utilizará 2b es la opción correcta para que especifique diferentes clases de coches. El freno en este caso será su automóvil & Quot; Estrategia & Quot; y puede establecer uno predeterminado.

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

Personalmente, siempre preferiría usar Interfaces sobre plantillas por varias razones:

  1. Plantillas compilando & amp; los errores de enlace a veces son crípticos
  2. Es difícil depurar un código basado en plantillas (al menos en IDE de Visual Studio)
  3. Las plantillas pueden agrandar sus binarios.
  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 programadores novatos.

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

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

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top