2010-06-22 10 views
7

Recentemente sono passato da Java e Ruby a C++ e con mia grande sorpresa devo ricompilare i file che usano l'interfaccia pubblica quando cambio la firma del metodo di un metodo privato, perché anche le parti private sono nel file .h.mantenendo le parti private esterne alle intestazioni C++: pura classe base virtuale vs pimpl

Ho trovato rapidamente una soluzione che è, credo, tipica per un programmatore Java: interfacce (= classi di base virtuali pure). Per esempio:

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); 
} 

L'unica seccatura qui, è che io non posso usare new, e devo invece chiamare BananaTree::create(). Non penso che sia davvero un problema, soprattutto perché mi aspetto comunque di usare molto le fabbriche.

Ora, i saggi della fama C++, tuttavia, hanno trovato un'altra soluzione, la pImpl idiom. Con questo, se ho ben capito, il mio codice sarà simile:

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); 
} 

Ciò significherebbe che devo implementare un decoratore -stante metodo di inoltro per ogni metodo pubblico di BananaTree, in questo caso getBanana. Questo mi sembra un livello aggiunto di complessità e sforzo di manutenzione che preferisco non richiedere.

Quindi, ora per la domanda: cosa c'è di sbagliato nell'approccio di classe virtuale pura? Perché l'approccio pImpl è molto meglio documentato? Mi sono perso qualcosa?

risposta

12

mi viene in mente alcune differenze:

Con la classe base virtuale si rompe alcune delle semantiche la gente si aspetta dalle classi ben educati C++:

mi si aspetterebbe (o richiedere, anche) il classe da un'istanza sullo stack, in questo modo:

BananaTree myTree("somename"); 

altrimenti, perdo Raii, e devo avviare manualmente il monitoraggio assegnazioni, che porta ad un sacco di mal di testa e perdite di memoria.

Mi aspetto inoltre che per copiare la classe, posso semplicemente fare questo

BananaTree tree2 = mytree; 

meno che, naturalmente, la copia non è consentita contrassegnando il costruttore privato di copia, in questo caso quella linea non sarà nemmeno la compilazione.

Nei casi precedenti, abbiamo ovviamente il problema che la classe di interfaccia in realtà non ha costruttori significativi. Ma se provassi ad usare codice come gli esempi precedenti, mi piacerebbe anche avere a che fare con molti problemi di slicing. Con oggetti polimorfici, in genere è necessario mantenere puntatori o riferimenti agli oggetti, per impedire l'affettatura. Come nel mio primo punto, questo non è generalmente auspicabile e rende la gestione della memoria molto più difficile.

Sarà un lettore del codice capire che un BananaTree fondamentalmente non funziona, che deve usare BananaTree* o BananaTree& invece?

Fondamentalmente, l'interfaccia semplicemente non gioca molto bene con i moderni C++, dove preferiamo

  • puntatori evitare il più possibile, e
  • pila-allocare tutti gli oggetti di beneficiare forma di vita automatica gestione.

A proposito, la classe di base virtuale ha dimenticato il distruttore virtuale. Questo è un bug chiaro.

Infine, una variante più semplice di Pimpl che a volte uso per ridurre la quantità di codice di codice è dare l'accesso "esterno" dell'oggetto ai membri dati dell'oggetto interno, in modo da evitare la duplicazione dell'interfaccia. O una funzione sull'oggetto esterno accede direttamente ai dati di cui ha bisogno direttamente dall'oggetto interno oppure chiama una funzione di supporto sull'oggetto interno, che non ha equivalenti sull'oggetto esterno.

Nel tuo esempio, è possibile rimuovere la funzione e Impl::getBanana, e invece implementare BananaTree::getBanana come questo:

allora basta implementare un getBanana funzione (nella classe BananaTree), e uno findBanana funzione (nella classe Impl).

1

In realtà, questa è solo una decisione progettuale da prendere. E anche se prendi la decisione "sbagliata", non è così difficile da cambiare.

pimpl viene inoltre utilizzato per fornire oggetti leggeri in pila o per presentare "copie" facendo riferimento allo stesso oggetto di implementazione.
Le funzioni di delega possono essere una seccatura, ma è un problema minore (semplice, quindi senza una reale complessità aggiunta), specialmente con classi limitate.

Le interfacce in C++ sono in genere più utilizzate in modalità strategiche in cui ci si aspetta di essere in grado di scegliere le implementazioni, anche se ciò non è richiesto.

Problemi correlati