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


49

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

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

Il confronto è:

  1. Costo :

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

  2. Ben capito :

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

  3. Prestazioni :

    Interfaccia modo prestazioni non peggiori del linguaggio PImpl, sia un ulteriore accesso alla memoria. Presumo che la performance sia la stessa.

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 utilizzando 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();

Allora, qual è il punto di PImpl?

Risposte:


50

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

Esistono tre motivi per utilizzare PImpl:

  1. Rendere l' interfaccia binaria (ABI) indipendente dai membri privati. È possibile aggiornare una libreria condivisa senza ricompilare il codice dipendente, ma solo finché l' interfaccia binaria rimane la stessa. Ora quasi tutte le modifiche all'intestazione, ad eccezione dell'aggiunta di una funzione non membro e dell'aggiunta di una funzione membro non virtuale, cambiano l'ABI. Il linguaggio PImpl sposta la definizione dei membri privati ​​nella fonte e disaccoppia l'ABI dalla loro definizione. Vedi Problema all'interfaccia binaria fragile

  2. Quando viene modificata un'intestazione, è necessario ricompilare tutte le origini inclusa quella. E la compilazione C ++ è piuttosto lenta. Quindi, spostando le definizioni dei membri privati ​​nella fonte, il linguaggio PImpl riduce il tempo di compilazione, poiché è necessario estrarre meno dipendenze nell'intestazione e ridurre i tempi di compilazione dopo le modifiche ancora di più poiché le persone a carico non devono essere ricompilate (ok, questo vale anche per l'interfaccia + funzione di fabbrica con classe di calcestruzzo nascosta).

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

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

  1. L'aggiunta di un metodo non virtuale non interrompe l'ABI, ma l'aggiunta di un metodo virtuale lo fa. Pertanto le interfacce non consentono affatto di aggiungere metodi, lo fa PImpl.

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

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

E ovviamente l'interfaccia non aiuta con la sicurezza delle eccezioni. Per questo è necessario il riferimento indiretto all'interno della classe.


Buon punto. L' aggiunta di una funzione pubblica in Implclasse non interromperà ABI, ma l'aggiunta di una funzione pubblica nell'interfaccia si interromperà ABI.
ZijingWu,

1
L'aggiunta di una funzione / metodo pubblici non deve interrompere l'ABI; i cambiamenti strettamente additivi non stanno rompendo i cambiamenti. Tuttavia, devi essere sempre così attento; il codice che media tra front-end e back-end deve far fronte a ogni tipo di divertimento con il versioning. Tutto diventa complicato. (Questo genere di cose è il motivo per cui un certo numero di progetti software preferisce il C al C ++; consente loro di avere una presa più stretta su esattamente ciò che è l'ABI.)
Donal Fellows

3
@DonalFellows: l'aggiunta di una funzione / metodo pubblici non interrompe l'ABI. L'aggiunta di un metodo virtuale lo fa e lo fa sempre, a meno che non venga impedito all'utente di ereditare l'oggetto (che PImpl ottiene).
Jan Hudec,

4
Per chiarire perché l' aggiunta di un metodo virtuale interrompe l'ABI. Ha a che fare con il collegamento. Il collegamento a metodi non virtuali è per nome, indipendentemente dal fatto che la libreria sia statica o dinamica. Aggiungere un altro metodo non virtuale significa semplicemente aggiungere un altro nome a cui collegarsi. Tuttavia i metodi virtuali sono indici in una vtable e il codice esterno si collega efficacemente per indice. L'aggiunta di un metodo virtuale può spostare altri metodi nella vtable senza che il codice esterno lo sappia.
SnakE

1
PImpl riduce anche il tempo di compilazione iniziale. Ad esempio, se la mia applicazione ha un campo di un certo tipo di modello (ad esempio unordered_set), senza Pimpl, ho bisogno di #include <unordered_set>a MyClass.h. Con PImpl, ho solo bisogno di includerlo nel .cppfile, non nel .hfile, e quindi tutto il resto che lo include ...
Martin C. Martin,

7

Voglio solo affrontare il tuo punto di esibizione. Se usi un'interfaccia, hai necessariamente creato funzioni virtuali che NON SARANNO sottolineate dall'ottimizzatore del compilatore. Le funzioni PIMPL possono (e probabilmente lo saranno, perché sono così brevi) essere incorporate (forse più di una volta se anche la funzione IMPL è piccola). Una chiamata di funzione virtuale non può essere ottimizzata se non si utilizzano ottimizzazioni di analisi del programma completo che richiedono molto tempo.

Se la tua classe PIMPL non viene utilizzata in modo critico per le prestazioni, questo punto non ha molta importanza, ma il tuo presupposto che le prestazioni siano le stesse vale solo in alcune situazioni, non in tutte.


1
Con l'uso dell'ottimizzazione del tempo di collegamento, la maggior parte dell'overhead dell'uso del linguaggio del pimpl viene rimosso. È possibile incorporare sia le funzioni del wrapper esterno che le funzioni di implementazione, facendo in modo che la funzione richiami efficacemente come una normale classe implementata all'interno di un'intestazione.
goji,

Questo è vero solo se si utilizza l'ottimizzazione del tempo di collegamento. Il punto di PImpl è che il compilatore non ha l'implementazione, nemmeno della funzione di inoltro. Il linker lo fa.
Martin C. Martin,

5

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

  1. La modifica delle variabili dei membri privati ​​di una classe non richiede la ricompilazione delle classi che dipendono da essa, quindi i tempi sono più rapidi e il FragileBinaryInterfaceProblem è ridotto.
  2. Il file di intestazione non ha bisogno di #includere le classi utilizzate 'per valore' nelle variabili dei membri privati, quindi i tempi di compilazione sono più veloci.

Il linguaggio Pimpl è un'ottimizzazione del tempo 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 concatena efficacemente il file header al file sorgente, quindi tieni presente che le unità C ++ pre-elaborate possono essere molto grandi. L'uso di Pimpl può migliorare i tempi di compilazione.

Esistono altre migliori tecniche di interruzione della dipendenza, che comportano il miglioramento del design. Cosa rappresentano i metodi privati? Qual è la singola responsabilità? Dovrebbero essere un'altra classe?


PImpl non è affatto un'ottimizzazione nel rispetto del tempo di esecuzione. Le allocazioni non sono banali e il pimpl significa allocazione aggiuntiva. È una tecnica che rompe le dipendenze e ottimizza solo i tempi di compilazione .
Jan Hudec,

@JanHudec chiarito.
Dave Hillier,

@DaveHillier, +1 per citare FragileBinaryInterfaceProblem
ZijingWu
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.