¿Cómo puedo evitar dynamic_cast en mi código C ++?
-
19-08-2019 - |
Pregunta
Digamos que tengo la siguiente estructura de clase:
class Car;
class FooCar : public Car;
class BarCar : public Car;
class Engine;
class FooEngine : public Engine;
class BarEngine : public Engine;
También demos un Car
un identificador a su Engine
. Se creará un FooCar
con un FooEngine *
y un BarCar
se creará con un BarEngine *
. ¿Hay alguna manera de organizar las cosas para que un objeto FooCar
pueda llamar a las funciones miembro de FooEngine
sin cambiar?
He aquí por qué la estructura de clases se presenta como está ahora:
- Todos los
Car
s tienen unEngine
. Además, unFooCar
solo usará unFooEngine
. - Hay datos y algoritmos compartidos por todos los
Engine
s que prefiero no copiar y pegar. - Es posible que desee escribir una función que requiera un
Engine
para conocer suCar
.
Tan pronto como escribí dynamic_cast
al escribir este código, supe que probablemente estaba haciendo algo mal. ¿Hay una mejor manera de hacer esto?
ACTUALIZACIÓN:
Basado en las respuestas dadas hasta ahora, me inclino por dos posibilidades:
- Haga que
Car
proporcione una función virtual pura degetEngine ()
. Eso permitiría queFooCar
yBarCar
tengan implementaciones que devuelvan el tipo correcto deEngine
. - Absorba toda la funcionalidad
Engine
en el árbol de herenciaCar
. ElEngine
se desglosó por razones de mantenimiento (para mantener las cosas delEngine
en un lugar separado). Es una compensación entre tener más clases pequeñas (pequeñas en líneas de código) versus tener menos clases grandes.
¿Existe una fuerte preferencia de la comunidad por una de estas soluciones? ¿Hay una tercera opción que no haya considerado?
Solución
Supongo que Car tiene un puntero del motor, y es por eso que te encuentras abatido.
Saque el puntero de su clase base y reemplácelo con una función virtual pura get_engine (). Entonces su FooCar y BarCar pueden mantener punteros al tipo de motor correcto.
(Edit)
Por qué esto funciona:
Dado que la función virtual Car :: get_engine ()
devolvería una referencia o un puntero , C ++ permitirá que las clases derivadas implementen esta función con un diferente tipo de retorno , siempre que el tipo de retorno solo difiera por ser un tipo más derivado.
Esto se llama tipos de retorno covariantes , y permitirá cada Car
escriba para devolver el Engine
correcto.
Otros consejos
Solo una cosa que quería agregar: este diseño ya me huele mal por lo que llamo árboles paralelos .
Básicamente, si terminas con jerarquías de clases paralelas (como lo has hecho con Car y Engine), entonces solo estás buscando problemas.
Volvería a pensar si Engine (e incluso Car) necesita tener subclases o si todas son instancias diferentes de las mismas clases base respectivas.
También puede modelar el tipo de motor de la siguiente manera
template<class EngineType>
class Car
{
protected:
EngineType* getEngine() {return pEngine;}
private:
EngineType* pEngine;
};
class FooCar : public Car<FooEngine>
class BarCar : public Car<BarEngine>
No veo por qué un automóvil no puede estar compuesto por un motor (si BarCar siempre contendrá BarEngine). El motor tiene una relación bastante fuerte con el automóvil. Preferiría:
class BarCar:public Car
{
//.....
private:
BarEngine engine;
}
¿Sería posible que FooCar use BarEngine?
Si no es así, es posible que desee utilizar un AbstractFactory para crear el objeto de automóvil correcto, con el motor correcto.
Puede almacenar FooEngine en FooCar, BarEngine en BarCar
class Car {
public:
...
virtual Engine* getEngine() = 0;
// maybe add const-variant
};
class FooCar : public Car
{
FooEngine* engine;
public:
FooCar(FooEngine* e) : engine(e) {}
FooEngine* getEngine() { return engine; }
};
// BarCar similarly
El problema con este enfoque es que obtener un motor es una llamada virtual (si le preocupa eso), y un método para configurar un motor en Car
requeriría downcasting.
Creo que depende de que Engine
solo sea utilizado de forma privada por Car
y sus hijos o si también desea usarlo en otros objetos.
Si las funcionalidades de Engine
no son específicas de Car
s, usaría un método virtual Engine * getEngine ()
en lugar de mantener un puntero en la clase base.
Si su lógica es específica de Car
s, preferiría poner los datos / lógica comunes de Engine
en un objeto separado (no necesariamente polimórfico) y mantener Implementación de FooEngine
y BarEngine
en su respectiva clase secundaria Car
.
Cuando el reciclaje de la implementación es más necesario que la herencia de la interfaz, la composición de objetos a menudo ofrece una mayor flexibilidad.
El COM de Microsoft es un poco torpe, pero tiene un concepto novedoso: si tiene un puntero a una interfaz de un objeto, puede consultarlo para ver si es compatible con otras interfaces que utilicen función QueryInterface . La idea es dividir su clase Engine en múltiples interfaces para que cada una pueda usarse de forma independiente.
Permitiendo que no me haya perdido algo, esto debería ser bastante trivial.
La mejor manera de hacer esto es crear funciones virtuales puras en Engine, que luego se requieren en clases derivadas que se van a instanciar.
Una solución de crédito adicional probablemente sería tener una interfaz llamada IEngine, que en su lugar pasa a su automóvil, y cada función en IEngine es puramente virtual. Podría tener un 'BaseEngine' que implemente algunas de las funcionalidades que desee (es decir, 'compartidas') y luego eliminar las hojas.
La interfaz es agradable si alguna vez tiene algo que quiere 'verse' como un motor, pero probablemente no lo es (es decir, simulacros de clases de prueba y demás).
¿Hay alguna manera de organizar las cosas para que un objeto FooCar pueda llamar a las funciones miembro de FooEngine sin desanimar?
Me gusta esto:
class Car
{
Engine* m_engine;
protected:
Car(Engine* engine)
: m_engine(engine)
{}
};
class FooCar : public Car
{
FooEngine* m_fooEngine;
public:
FooCar(FooEngine* fooEngine)
: base(fooEngine)
, m_fooEngine(fooEngine)
{}
};