Qual è il punto del pattern PImpl mentre possiamo usare l'interfaccia per lo stesso scopo in C ++?

45

Vedo un sacco di codice sorgente che usa l'idioma PImpl in C ++. Presumo che il suo scopo sia quello di nascondere i dati privati / tipo / implementazione, in modo che possa rimuovere la dipendenza e quindi ridurre il tempo di compilazione e il problema dell'intestazione.

Ma anche le classi di interfaccia / pure-abstract in C ++ hanno questa capacità, possono anche essere usate per nascondere dati / tipo / implementazione. E per consentire al chiamante di vedere l'interfaccia solo quando si crea un oggetto, possiamo dichiarare un metodo factory nell'intestazione dell'interfaccia.

Il confronto è:

  1. Costo :

    Il costo della modalità di interfaccia è inferiore, perché non è nemmeno necessario ripetere l'implementazione della funzione wrapper pubblico void Bar::doWork() { return m_impl->doWork(); } , è sufficiente definire la firma nell'interfaccia.

  2. Ben comprensibile :

    La tecnologia dell'interfaccia è meglio compresa da ogni sviluppatore C ++.

  3. Performance :

    Le prestazioni dell'interfaccia non sono peggiori dell'idioma PImpl, sia un accesso extra alla memoria. Presumo che le prestazioni siano le stesse.

Di seguito è riportato lo pseudocodice per illustrare la mia domanda:

// Forward declaration can help you avoid include BarImpl header, and those included in BarImpl header.
class BarImpl;
class Bar
{
public:
    // public functions
    void doWork();
private:
    // You don't need to compile Bar.cpp after changing the implementation in BarImpl.cpp
    BarImpl* m_impl;
};

Lo stesso scopo può essere implementato usando l'interfaccia:

// Bar.h
class IBar
{
public:
    virtual ~IBar(){}
    // public functions
    virtual void doWork() = 0;
};

// to only expose the interface instead of class name to caller
IBar* createObject();

Quindi qual è il punto di PImpl?

    
posta ZijingWu 03.10.2013 - 13:00
fonte

3 risposte

43

In primo luogo, PImpl viene solitamente utilizzato per le classi non polimorfiche. E quando una classe polimorfica ha PImpl, di solito rimane polimorfa, cioè implementa ancora interfacce e sovrascrive i metodi virtuali dalla classe base e così via. L'implementazione più semplice di PImpl è l'interfaccia non , è una semplice classe che contiene direttamente i membri!

Ci sono tre motivi per usare PImpl:

  1. Rendere l'interfaccia binario (ABI) indipendente dai membri privati. È possibile aggiornare una libreria condivisa senza ricompilare il codice dipendente, ma solo fino a quando l'interfaccia binario rimane la stessa. Ora quasi tutte le modifiche all'intestazione, tranne l'aggiunta di una funzione non membro e l'aggiunta di una funzione membro non virtuale, cambia l'ABI. L'idioma PImpl sposta la definizione dei membri privati nella sorgente e quindi disaccoppia l'ABI dalla loro definizione. Vedi Problema di interfaccia binaria fragile

  2. Quando cambia l'intestazione, tutte le fonti comprese devono essere ricompilate. E la compilazione C ++ è piuttosto lenta. Quindi spostando le definizioni dei membri privati nell'origine, l'idioma PImpl riduce il tempo di compilazione, dal momento che è necessario tirare nell'intestazione meno dipendenze e riduce il tempo di compilazione dopo le modifiche, anche di più, poiché i dipendenti non devono essere ricompilati (ok, questo si applica all'interfaccia + funzione di fabbrica con classe concreta nascosta anche).

  3. Per molte classi in C ++, la sicurezza delle eccezioni è una proprietà importante. Spesso è necessario comporre più classi in una in modo tale che se durante l'operazione su più di un membro getta, nessuno dei membri viene modificato o si ha un'operazione che lascerà il membro in stato incoerente se getta e si ha bisogno che l'oggetto contenente rimanga coerente. In tal caso, l'operazione viene implementata creando una nuova istanza di PImpl e scambiandoli quando l'operazione ha esito positivo.

In realtà l'interfaccia può anche essere utilizzata solo per nascondere l'implementazione, ma ha i seguenti svantaggi:

  1. L'aggiunta di un metodo non virtuale non interrompe l'ABI, ma l'aggiunta di uno virtuale lo fa. Le interfacce pertanto non consentono affatto l'aggiunta di metodi, fa PImpl.

  2. L'interfaccia può essere utilizzata solo tramite puntatore / riferimento, pertanto l'utente deve occuparsi della corretta gestione delle risorse. D'altra parte le classi che usano PImpl sono ancora tipi di valore e gestiscono internamente le risorse.

  3. L'implementazione nascosta non può essere ereditata, classe con PImpl can.

E, naturalmente, l'interfaccia non aiuterà con la sicurezza delle eccezioni. Hai bisogno della direzione indiretta all'interno della classe per questo.

    
risposta data 03.10.2013 - 13:17
fonte
6

Voglio solo indirizzare il tuo punto di prestazione. Se si utilizza un'interfaccia, è necessario aver creato funzioni virtuali che NON saranno in linea con l'ottimizzatore del compilatore. Le funzioni PIMPL possono (e probabilmente lo saranno, perché sono così corte) essere inline (forse più di una volta se anche la funzione IMPL è piccola). Non è possibile ottimizzare una chiamata alla funzione virtuale a meno che non si utilizzino ottimizzazioni complete dell'analisi del programma che richiedono molto tempo.

Se la tua classe PIMPL non viene utilizzata in modo critico dal punto di vista delle prestazioni, questo punto non ha molta importanza, ma il tuo presupposto che la performance sia la stessa vale solo in alcune situazioni, non in tutte.

    
risposta data 24.10.2013 - 00:06
fonte
5

Questa pagina risponde alla tua domanda. Molte persone sono d'accordo con la tua affermazione. L'utilizzo di Pimpl presenta i seguenti vantaggi rispetto ai campi / membri privati.

  1. Changing private member variables of a class does not require recompiling classes that depend on it, thus make times are faster, and the FragileBinaryInterfaceProblem is reduced.
  2. The header file does not need to #include classes that are used 'by value' in private member variables, thus compile times are faster.

L'idioma Pimpl è un'ottimizzazione in fase di compilazione, una tecnica di rottura delle dipendenze. Riduce i file di intestazione di grandi dimensioni. Riduce l'intestazione solo all'interfaccia pubblica. La lunghezza dell'intestazione è importante in C ++ quando pensi a come funziona. # include concatenando in modo efficace il file di intestazione al file sorgente, quindi tenetelo a mente dopo che le unità C ++ pre-elaborate possono essere molto grandi. L'utilizzo di Pimpl può migliorare i tempi di compilazione.

Esistono altre tecniche di rottura delle dipendenze migliori, che implicano il miglioramento del design. Cosa rappresentano i metodi privati? Qual è la singola responsabilità? Dovrebbero essere un'altra classe?

    
risposta data 03.10.2013 - 13:16
fonte

Leggi altre domande sui tag