2010-03-25 8 views

Respuesta

6

se abordó en GotW #31. Resumen:

Hay tres razones principales por las que puede hacer esto. # 1 es un lugar común, # 2 es bastante raro, y # 3 es una solución alternativa utilizada ocasionalmente por los programadores avanzados que trabajan con compiladores más débiles .

La mayoría de los programadores solo deben usar el n. ° 1.

... Que es para destructores virtuales puros.

+0

¡Bonito hallazgo! +1 FTW –

+0

No estoy de acuerdo con 2 como bastante raro. Prefiero obligar al desarrollador a declarar explícitamente que desea usar el valor predeterminado. Demasiados se desarrollan compilando y se detienen tan pronto como lo hace, al menos de esta manera tengo la esperanza de que puedan leer el fragmento para que sepan lo que está haciendo el valor predeterminado. –

0

De esta manera puede proporcionar una implementación que funcione, pero aún así requiere que el implementador de clase hijo llame explícitamente a esa implementación.

1

Wikipedia resume esto muy bien:

Aunque los métodos virtuales puros normalmente no tienen aplicación en la clase que los declara, métodos virtuales puros en C++ están permitidos para contener una implementación en su declarando la clase, proporcionando el comportamiento predeterminado de Fallback o al que puede delegar una clase derivada , si corresponde.

Normalmente no necesita proporcionar implementaciones de la clase base para virtuales puros. Pero hay una excepción: pure virtual destructors. De hecho, si su clase base tiene un destructor virtual puro, it must have an implementation. ¿Por qué necesitarías un destructor virtual puro en lugar de uno virtual? Típicamente, para hacer una clase base abstracta sin requerir la implementación de ningún otro método. Por ejemplo, en una clase en la que puede razonablemente usar la implementación predeterminada para cualquier método, pero todavía no quiere que las personas creen una instancia de la clase base, puede marcar solo el destructor como puramente virtual.

EDIT:

Aquí hay algo de código que ilustra algunas de las formas para llamar a la implementación base:

#include <iostream> 
using namespace std; 

class Base 
{ 
public: 
    virtual void DoIt() = 0; 
}; 

class Der : public Base 
{ 
public: 
    void DoIt(); 
}; 

void Base::DoIt() 
{ 
    cout << "Base" << endl; 
} 

void Der::DoIt() 
{ 
    cout << "Der" << endl; 
    Base::DoIt(); 
} 

int main() 
{ 
    Der d; 
    Base* b = &d; 

    d.DoIt(); 
    b->DoIt(); // note that Der::DoIt is still called 
    b->Base::DoIt(); 


    return 0; 
} 
+0

Hace _prevenir_ evitar que se cree una instancia de la clase base. – KitsuneYMG

+0

@kts: sí, por supuesto que sí. ¿Dije que no? –

2

No hay conflicto con los dos conceptos, aunque rara vez se utilizan juntos (como se puristas OO no puede conciliarlo, pero eso está más allá del alcance de esta pregunta/respuesta).

La idea es que la función pure virtual tenga una implementación y, al mismo tiempo, obligue a las subclases a anular esa implementación. Las subclases pueden invocar la función de clase base para proporcionar algún comportamiento predeterminado. La base no se puede instanciar (es "abstracta") porque la (s) función (es) virtual (es) es pura aunque pueda tener una implementación.

+0

@STingRaySC: Gracias por aclararme esto. Siempre pensé que una función virtual pura no podría tener una implementación. pero realmente solo dice que un tipo derivado debe anularlo.Y ahora puedo ver por qué mi respuesta no respondió la pregunta de OP. Perdón por mi testarudez al entender esta distinción. –

+0

@Brian: sin preocupaciones. Es algo poco conocido. Me alegra que finalmente lo hayas conseguido. –

0

Bueno, tenemos algunas buenas respuestas ya .. Estoy a frenar en la escritura ..

Mi pensamiento sería, por ejemplo, una función init que tiene try {} catch {}, lo que significa que shouldn 't ser colocado en un constructor:

class A { 
public: 
    virtual bool init() = 0 { 
     ... // initiate stuff that couldn't be made in constructor 
    } 
}; 

class B : public A{ 
public: 
    bool init(){ 
     ... 
     A::init(); 
    } 
}; 
7

En Efectivo C++, Scott Meyers da el ejemplo de que es útil cuando se reutiliza el código a través de la herencia. Empieza con esto:

struct Airplane { 
    virtual void fly() { 
     // fly the plane 
    } 
    ... 
}; 

struct ModelA : Airplane { ... }; 
struct ModelB : Airplane { ... }; 

Ahora, ModelA y ModelB se vuelan la misma manera, y que cree que es una forma común de volar un avión, por lo que el código está en la clase base. Sin embargo, no todos los aviones se vuelan de esa manera, y tenemos la intención de que los planos sean polimórficos, por lo que es virtual.

Ahora añadimos ModelC, que debe ser trasladado de manera diferente, pero comete un error:

struct ModelC : Airplane { ... (no fly function) }; 

Vaya. ModelC va a estrellarse. Meyers preferiría que el compilador nos advirtiera de nuestro error.

Por lo tanto, se hace fly pura virtual en avión con una aplicación, y luego en ModelA y ModelB, poner:

void fly() { Airplane::fly(); } 

Ahora, a menos que explicitamente estado en nuestra clase derivada que queremos que el vuelo predeterminado comportamiento, no lo conseguimos. Entonces, en lugar de solo la documentación que nos dice todas las cosas que debemos verificar sobre nuestro nuevo modelo de avión, el compilador también nos dice.

Esto hace el trabajo, pero creo que es un poco débil. Idealmente, tenemos una mezcla de BoringlyFlyable que contiene la implementación predeterminada de fly, y reutilizamos el código de esa manera, en lugar de poner código en una clase base que asume ciertas cosas sobre los aviones que no son requisitos de los aviones. Pero eso requiere CRTP si la función fly realmente hace algo significativo:

#include <iostream> 

struct Wings { 
    void flap() { std::cout << "flapping\n"; } 
}; 

struct Airplane { 
    Wings wings; 
    virtual void fly() = 0; 
}; 

template <typename T> 
struct BoringlyFlyable { 
    void fly() { 
     // planes fly by flapping their wings, right? Same as birds? 
     // (This code may need tweaking after consulting the domain expert) 
     static_cast<T*>(this)->wings.flap(); 
    } 
}; 

struct PlaneA : Airplane, BoringlyFlyable<PlaneA> { 
    void fly() { BoringlyFlyable<PlaneA>::fly(); } 
}; 

int main() { 
    PlaneA p; 
    p.fly(); 
} 

Cuando Planea declara herencia de BoringlyFlyable, que hace valer a través de interfaz que es válida para volar en la forma predeterminada. Tenga en cuenta que BoringlyFlyable podría definir funciones virtuales puras propias: quizás getWings sería una buena abstracción. Pero como es una plantilla, no es necesario.

Tengo la sensación de que este patrón puede reemplazar todos los casos en los que hubiera proporcionado una función virtual pura con una implementación: la implementación puede ir en un mixin, que las clases pueden heredar si lo desean. Pero no puedo probar de inmediato que (por ejemplo, si Airplane::fly utiliza miembros privados, entonces se requiere un rediseño considerable para hacerlo de esta manera), y podría decirse que CRTP es un poco de alta potencia para el principiante de todos modos. También hay un poco más de código que no agrega funcionalidad o seguridad, simplemente hace explícito lo que ya está implícito en el diseño de Meyer, que algunas cosas pueden volar simplemente batiendo sus alas mientras que otras necesitan hacer otras cosas. Entonces mi versión de ninguna manera es un shoo-in total.

+0

Tanto como las plantillas similares, forzar la herencia múltiple en nosotros parece bastante molesto. ¿Por qué no simplemente proporcionar una función de plantilla? 'plantilla void fly (T * t) {t-> wings.flap(); } ' Tiendo a esconderme de la herencia múltiple en la medida de lo posible. –

+0

Es solo que las mezclas no funcionan tan bien con las funciones virtuales como te gustaría. Una opción es hacer que BoringlyFlyable herede Airplane (quizás como un parámetro de plantilla, para mantenerlo flexible), por lo que PlaneA no hereda directamente de Airplane, solo a través de 'BoringlyFlyable '. De esta manera no hay MI, y PlaneA no tiene que definir explícitamente 'fly', lo que es una ventaja sobre mi código existente y la plantilla de función' fly_boringly', que tienen que ser llamados desde 'PlaneA'. –

Cuestiones relacionadas