2010-06-22 12 views
7

Recientemente he cambiado de nuevo desde Java y Ruby a C++, y para mi sorpresa me tienen que volver a compilar los archivos que utilizan la interfaz pública cuando cambio el método de la firma de un método privado, porque también las partes privadas están en el archivo .h.mantenimiento de partes privadas fuera C++ cabeceras: pura clase base virtual vs pimpl

rápidamente me ocurrió una solución que es, supongo, típico de un programador de Java: interfaces (= clases base virtuales puras). Por ejemplo:

BananaTree.h:

class Banana; 

class BananaTree 
{ 
public: 
    virtual Banana* getBanana(std::string const& name) = 0; 

    static BananaTree* create(std::string const& name); 
}; 

BananaTree.cpp:

class BananaTreeImpl : public BananaTree 
{ 
private: 
    string name; 

    Banana* findBanana(string const& name) 
    { 
    return //obtain banana, somehow; 
    } 

public: 
    BananaTreeImpl(string name) 
    : name(name) 
    {} 

    virtual Banana* getBanana(string const& name) 
    { 
    return findBanana(name); 
    } 
}; 

BananaTree* BananaTree::create(string const& name) 
{ 
    return new BananaTreeImpl(name); 
} 

El único inconveniente aquí es que no puedo usar new, y en su lugar debe llamar BananaTree::create(). No creo que eso sea realmente un problema, especialmente porque espero usar muchas fábricas de todos modos.

Ahora, los sabios de la fama C++, sin embargo, ocurrió otra solución, el pImpl idiom. Con eso, si he entendido bien, mi código se vería así:

BananaTree.h:

class BananaTree 
{ 
public: 
    Banana* addStep(std::string const& name); 

private: 
    struct Impl; 
    shared_ptr<Impl> pimpl_; 
}; 

BananaTree.cpp:

struct BananaTree::Impl 
{ 
    string name; 

    Banana* findBanana(string const& name) 
    { 
    return //obtain banana, somehow; 
    } 

    Banana* getBanana(string const& name) 
    { 
    return findBanana(name); 
    } 

    Impl(string const& name) : name(name) {} 
} 

BananaTree::BananaTree(string const& name) 
    : pimpl_(shared_ptr<Impl>(new Impl(name))) 
{} 

Banana* BananaTree::getBanana(string const& name) 
{ 
    return pimpl_->getBanana(name); 
} 

Esto significa que tenga que aplicar un decorador método de reenvío de estilo para cada método público de BananaTree, en este caso getBanana. Esto suena como un nivel agregado de complejidad y esfuerzo de mantenimiento que prefiero no requerir.

lo tanto, ahora la pregunta: ¿Qué está mal con el enfoque de clase virtual pura? ¿Por qué el enfoque de pImpl está mucho mejor documentado? ¿Yo me perdí algo?

Respuesta

12

puedo pensar en algunas diferencias:

Con la clase base virtual a romper algunas de la semántica gente espera de clases de buen comportamiento de C++:

Yo esperaría (o requerir, incluso) la clase que se crea una instancia en la pila, así:

BananaTree myTree("somename"); 

de lo contrario, no pierda yo RAII, y tengo que iniciar manualmente el seguimiento de las asignaciones, lo que lleva a un montón de dolores de cabeza y pérdidas de memoria.

También espero que para copiar la clase, simplemente puedo hacer esto

BananaTree tree2 = mytree; 

a menos, por supuesto, no está permitido copiar marcando la privada constructor de copia, en cuyo caso esa línea ni siquiera compilar.

En los casos anteriores, obviamente tenemos el problema de que su clase de interfaz realmente no tiene constructores significativos. Pero si traté de usar código como los ejemplos anteriores, también me encontré con muchos problemas de corte. Con objetos polimórficos, generalmente se requiere que contenga punteros o referencias a los objetos para evitar cortar. Como en mi primer punto, esto generalmente no es deseable, y hace que la gestión de la memoria sea mucho más difícil.

¿El lector de su código comprenderá que un BananaTree básicamente no funciona, que tiene que usar BananaTree* o BananaTree& en su lugar?

Básicamente, su interfaz simplemente no juega tan bien con C moderna ++, donde preferimos

  • punteros evitar en la medida de lo posible, y
  • pila-asignar todos los objetos para beneficiar forma de por vida automática administración.

Por cierto, su clase base virtual olvidó el destructor virtual. Ese es un error claro.

Finalmente, una variante más simple de pimpl que a veces uso para reducir la cantidad de código repetitivo es dar acceso al objeto "externo" a los miembros de datos del objeto interno, para evitar duplicar la interfaz. O bien una función en el objeto externo simplemente accede directamente a los datos que necesita del objeto interno, o llama a una función auxiliar en el objeto interno, que no tiene equivalente en el objeto externo.

En el ejemplo, se puede quitar la función y Impl::getBanana, y en lugar de poner en práctica BananaTree::getBanana así:

a continuación, sólo tiene que implementar uno getBanana función (en la clase BananaTree), y uno findBanana función (en la clase Impl).

1

En realidad, esto es solo una decisión de diseño para hacer. E incluso si tomas la decisión "incorrecta", no es tan difícil cambiarla.

pimpl también se utiliza para proporcionar objetos Ligthweight en la pila o para presentar "copias" al hacer referencia al mismo objeto aplicación.
La delegación de funciones puede ser una molestia, pero es un problema menor (simple, por lo que no hay una complejidad añadida real), especialmente con clases limitadas.

interfaces en C++ son por lo general más utilizado de una manera similar a la estrategia en la que esperan ser capaces de elegir implementaciones, aunque eso no es necesario.

Cuestiones relacionadas