L'idioma pImpl è davvero usato nella pratica?


165

Sto leggendo il libro "Eccezionale C ++" di Herb Sutter e in quel libro ho imparato a conoscere il linguaggio pImpl. Fondamentalmente, l'idea è quella di creare una struttura per gli privateoggetti di un classe allocarli dinamicamente per ridurre i tempi di compilazione (e anche nascondere le implementazioni private in un modo migliore).

Per esempio:

class X
{
private:
  C c;
  D d;  
} ;

potrebbe essere modificato in:

class X
{
private:
  struct XImpl;
  XImpl* pImpl;       
};

e, nel CPP, la definizione:

struct X::XImpl
{
  C c;
  D d;
};

Questo sembra piuttosto interessante, ma non ho mai visto questo tipo di approccio prima, né nelle aziende in cui ho lavorato, né in progetti open source che ho visto il codice sorgente. Quindi, mi chiedo che questa tecnica sia davvero utilizzata nella pratica?

Dovrei usarlo ovunque o con cautela? E si consiglia di utilizzare questa tecnica nei sistemi embedded (dove le prestazioni sono molto importanti)?


È essenzialmente lo stesso di decidere che X è un'interfaccia (astratta) e Ximpl è l'implementazione? struct XImpl : public X. Mi sembra più naturale. C'è qualche altro problema che mi sono perso?
Aaron McDaid il

@AaronMcDaid: è simile, ma presenta i vantaggi che (a) le funzioni membro non devono essere virtuali e (b) non è necessaria una factory o la definizione della classe di implementazione per istanziarla.
Mike Seymour,

2
@AaronMcDaid L'idioma del pimpl evita le chiamate di funzione virtuale. È anche un po 'più C ++ - ish (per alcuni concetti di C ++ - ish); invochi costruttori, piuttosto che funzioni di fabbrica. Ho usato entrambi, a seconda di ciò che è nella base di codice esistente --- l'idioma del brufolo (originariamente chiamato il linguaggio del gatto del Cheshire, e precedente la descrizione di Herb di esso di almeno 5 anni) sembra avere una storia più lunga ed essere più ampiamente usato in C ++, ma per il resto entrambi funzionano.
James Kanze,

30
In C ++, pimpl dovrebbe essere implementato con const unique_ptr<XImpl>piuttosto che XImpl*.
Neil G,

1
"mai visto questo tipo di approccio prima, né nelle aziende in cui ho lavorato, né in progetti open source". Qt non lo usa quasi mai.
ManuelSchneid3r

Risposte:


132

Quindi, mi chiedo che questa tecnica sia davvero utilizzata nella pratica? Dovrei usarlo ovunque o con cautela?

Certo che è usato. Lo uso nel mio progetto, in quasi tutte le classi.


Motivi per l'utilizzo del linguaggio PIMPL:

Compatibilità binaria

Quando stai sviluppando una libreria, puoi aggiungere / modificare campi XImplsenza interrompere la compatibilità binaria con il tuo client (il che significherebbe arresti anomali!). Poiché il layout binario della Xclasse non cambia quando si aggiungono nuovi campi alla Ximplclasse, è sicuro aggiungere nuove funzionalità alla libreria negli aggiornamenti delle versioni minori.

Ovviamente, puoi anche aggiungere nuovi metodi non virtuali pubblici / privati ​​a X/ XImplsenza interrompere la compatibilità binaria, ma è alla pari con la tecnica standard di intestazione / implementazione.

Dati nascosti

Se stai sviluppando una libreria, specialmente una proprietaria, potrebbe essere desiderabile non rivelare quali altre librerie / tecniche di implementazione sono state utilizzate per implementare l'interfaccia pubblica della tua libreria. O a causa di problemi di proprietà intellettuale o perché ritieni che gli utenti potrebbero essere tentati di fare ipotesi pericolose sull'implementazione o semplicemente rompere l'incapsulamento usando terribili trucchi di lancio. PIMPL risolve / mitiga questo.

Tempo di compilazione

Il tempo di compilazione è ridotto, poiché è Xnecessario ricostruire solo il file di origine (implementazione) quando si aggiungono / rimuovono campi e / o metodi alla XImplclasse (che associa all'aggiunta di campi / metodi privati ​​nella tecnica standard). In pratica, è un'operazione comune.

Con la tecnica standard di intestazione / implementazione (senza PIMPL), quando si aggiunge un nuovo campo a X, tutti i client che allocano X(sia in stack che in heap) devono essere ricompilati, perché devono regolare le dimensioni dell'allocazione. Bene, ogni cliente che non mai allocare X anche bisogno di essere ricompilato, ma è solo in testa (il codice risultante sul lato client sarà lo stesso).

Inoltre, con la separazione standard di intestazione / implementazione XClient1.cppdeve essere ricompilata anche quando è X::foo()stato aggiunto Xe X.hmodificato un metodo privato , anche se XClient1.cppnon è possibile chiamarlo per motivi di incapsulamento! Come sopra, è puro sovraccarico ed è correlato a come funzionano i sistemi di build C ++ nella vita reale.

Naturalmente, la ricompilazione non è necessaria quando si modifica semplicemente l'implementazione dei metodi (perché non si tocca l'intestazione), ma è alla pari con l'intestazione / tecnica di implementazione standard.


Si consiglia di utilizzare questa tecnica nei sistemi embedded (dove le prestazioni sono molto importanti)?

Dipende da quanto è potente il tuo obiettivo. Tuttavia l'unica risposta a questa domanda è: misurare e valutare ciò che si guadagna e si perde. Inoltre, tieni presente che se non stai pubblicando una libreria destinata ai client per essere utilizzata nei sistemi incorporati, si applica solo il vantaggio del tempo di compilazione!


16
+1 perché è ampiamente utilizzato nell'azienda per cui lavoro anche per gli stessi motivi.
Benoit,

9
inoltre, compatibilità binaria
Ambroz Bizjak il

9
Nella libreria Qt questo metodo viene utilizzato anche in situazioni di puntatore intelligente. Quindi QString mantiene i suoi contenuti come una classe immutabile internamente. Quando la classe pubblica viene "copiata", viene copiato il puntatore del membro privato anziché l'intera classe privata. Queste classi private utilizzano quindi anche i puntatori intelligenti, quindi in pratica ottieni la garbage collection con la maggior parte delle classi, oltre alle prestazioni notevolmente migliorate grazie alla copia del puntatore invece della copia completa della classe
Timothy Baldridge,

8
Ancora di più, con l'idioma del pimpl Qt può mantenere la compatibilità binaria sia in avanti che indietro all'interno di una singola versione principale (nella maggior parte dei casi). IMO questo è di gran lunga il motivo più significativo per usarlo.
Whitequark,

1
È anche utile per l'implementazione di codice specifico per la piattaforma, poiché puoi conservare la stessa API.
doc

49

Sembra che molte librerie là fuori lo usino per rimanere stabili nella loro API, almeno per alcune versioni.

Ma come per tutte le cose, non dovresti mai usare nulla ovunque senza cautela. Pensa sempre prima di usarlo. Valuta quali vantaggi ti offre e se valgono il prezzo che paghi.

I vantaggi che può darti sono:

  • aiuta a mantenere la compatibilità binaria delle librerie condivise
  • nascondendo alcuni dettagli interni
  • cicli di ricompilazione decrescenti

Questi possono o meno essere dei veri vantaggi per te. Come per me, non mi interessa qualche minuto di ricompilazione. Anche gli utenti finali di solito non lo fanno, poiché lo compilano sempre una volta e dall'inizio.

I possibili svantaggi sono (anche qui, a seconda dell'implementazione e se si tratta di svantaggi reali per te):

  • Aumento dell'utilizzo della memoria dovuto a più allocazioni rispetto alla variante ingenua
  • maggiore sforzo di manutenzione (è necessario scrivere almeno le funzioni di inoltro)
  • perdita di prestazioni (il compilatore potrebbe non essere in grado di incorporare cose come in un'implementazione ingenua della tua classe)

Quindi, dai con attenzione a tutto un valore e valutalo da solo. Per me, quasi sempre risulta che l'uso del linguaggio del brufolo non vale la pena. C'è solo un caso in cui lo uso personalmente (o almeno qualcosa di simile):

Il mio wrapper C ++ per la statchiamata Linux . Qui la struttura dell'intestazione C può essere diversa, a seconda di ciò che #definesè impostato. E poiché la mia intestazione wrapper non può controllarli tutti, io solo #include <sys/stat.h>nel mio .cxxfile ed evito questi problemi.


2
Dovrebbe quasi sempre essere usato per le interfacce di sistema, per rendere indipendente il sistema di codici di interfaccia. La mia Fileclasse (che espone gran parte delle informazioni statrestituite in Unix) utilizza la stessa interfaccia in Windows e Unix, ad esempio.
James Kanze,

5
@JamesKanze: Anche lì, per prima cosa, mi siedo per un attimo a pensare se non è forse sufficiente avere qualche #ifdefs per rendere la confezione il più sottile possibile. Ma ognuno ha obiettivi diversi, l'importante è prendersi il tempo di pensarci invece di seguire ciecamente qualcosa.
PlasmaHH,

31

Concordo con tutti gli altri sui prodotti, ma lasciatemi mettere in evidenza un limite: non funziona bene con i modelli .

Il motivo è che l'istanza del modello richiede la dichiarazione completa disponibile nel luogo in cui è avvenuta l'istanza. (E questo è il motivo principale per cui non vedi i metodi modello definiti nei file CPP)

È ancora possibile fare riferimento a sottoclassi di modelli, ma poiché è necessario includerli tutti, si perde ogni vantaggio di "disaccoppiamento dell'implementazione" sulla compilazione (evitando di includere ovunque tutto il codice specifico del platoform, accorciando la compilazione).

È un buon paradigma per OOP classico (basato sull'ereditarietà) ma non per la programmazione generica (basata sulla specializzazione).


4
Devi essere più preciso: non c'è assolutamente alcun problema quando si usano le classi PIMPL come argomenti di tipo template. Solo se la classe di implementazione stessa deve essere parametrizzata sugli argomenti del modello della classe esterna, non può più essere nascosta dall'intestazione dell'interfaccia, anche se è ancora una classe privata. Se riesci a eliminare l'argomento template, puoi sicuramente fare PIMPL "corretto". Con l'eliminazione del tipo è anche possibile eseguire la PIMPL in una classe di base non modello e quindi farne derivare la classe modello.
Ripristina Monica

22

Altre persone hanno già fornito gli aspetti tecnici positivi / negativi, ma penso che vale la pena notare quanto segue:

Innanzitutto, non essere dogmatico. Se pImpl funziona per la tua situazione, usalo - non usarlo solo perché "è OO migliore poiché nasconde davvero l' implementazione" ecc. Citando le Domande frequenti su C ++:

l'incapsulamento è per il codice, non per le persone ( fonte )

Solo per darvi un esempio di software open source in cui viene utilizzato e perché: OpenThreads, la libreria di threading utilizzata da OpenSceneGraph . L'idea principale è quella di rimuovere dall'intestazione (ad es. <Thread.h>) Tutto il codice specifico della piattaforma, poiché le variabili di stato interne (ad es. Handle di thread) differiscono da piattaforma a piattaforma. In questo modo si può compilare il codice sulla propria libreria senza alcuna conoscenza delle idiosincrasie delle altre piattaforme, perché tutto è nascosto.


12

Considererei principalmente PIMPL per le classi esposte ad essere utilizzate come API da altri moduli. Ciò ha molti vantaggi, poiché rende la ricompilazione delle modifiche apportate all'implementazione di PIMPL non influisce sul resto del progetto. Inoltre, per le classi API promuovono una compatibilità binaria (le modifiche nell'implementazione di un modulo non influiscono sui client di tali moduli, non devono essere ricompilate poiché la nuova implementazione ha la stessa interfaccia binaria - l'interfaccia esposta da PIMPL).

Per quanto riguarda l'utilizzo di PIMPL per ogni classe, prenderei in considerazione la cautela perché tutti questi vantaggi hanno un costo: è necessario un ulteriore livello di riferimento indiretto per accedere ai metodi di implementazione.


"è necessario un ulteriore livello di riferimento indiretto per accedere ai metodi di implementazione." È?
xaxxon,

@xaxxon sì, lo è. il pimpl è più lento se i metodi sono di basso livello. non usarlo mai per cose che vivono in un circuito ristretto, per esempio.
Erik Aronesty,

@xaxxon Direi che in generale è richiesto un livello aggiuntivo. Se viene eseguito l'allineamento di no. Ma l'inlinazione non sarebbe un'opzione nel codice compilato in una diversa DLL.
Ghita,

5

Penso che questo sia uno degli strumenti fondamentali per il disaccoppiamento.

Stavo usando pimpl (e molti altri modi di dire di C ++ eccezionale) sul progetto incorporato (SetTopBox).

Lo scopo particolare di questo idoim nel nostro progetto era nascondere i tipi utilizzati dalla classe XImpl. In particolare, l'abbiamo usato per nascondere i dettagli delle implementazioni per hardware diverso, in cui verrebbero inserite diverse intestazioni. Avevamo implementazioni diverse delle classi XImpl per una piattaforma e diverse per l'altra. La disposizione della classe X è rimasta la stessa indipendentemente dal platfrom.


4

Usavo questa tecnica molto in passato, ma poi mi sono ritrovato ad allontanarmi da essa.

Naturalmente è una buona idea nascondere i dettagli dell'implementazione agli utenti della tua classe. Tuttavia, puoi farlo anche facendo in modo che gli utenti della classe utilizzino un'interfaccia astratta e che i dettagli dell'implementazione siano la classe concreta.

I vantaggi di pImpl sono:

  1. Supponendo che vi sia una sola implementazione di questa interfaccia, è più chiaro non usando un'implementazione astratta di classe / calcestruzzo

  2. Se si dispone di una suite di classi (un modulo) in modo che diverse classi accedano allo stesso "impl", ma gli utenti del modulo useranno solo le classi "scoperte".

  3. Nessuna v-table se questa è considerata una cosa negativa.

Gli svantaggi che ho riscontrato di pImpl (dove l'interfaccia astratta funziona meglio)

  1. Mentre potresti avere una sola implementazione "di produzione", usando un'interfaccia astratta puoi anche creare un'implementazione "finta" che funziona nel test unitario.

  2. (Il problema più grande). Prima dei giorni di unique_ptr e di traslochi avevi scelte limitate su come conservare il pImpl. Un puntatore non elaborato e si sono verificati problemi di non copiabilità della classe. Un vecchio auto_ptr non funzionerebbe con la classe dichiarata in avanti (non su tutti i compilatori comunque). Quindi le persone hanno iniziato a usare shared_ptr, il che è stato bello nel rendere la tua classe copiabile, ma ovviamente entrambe le copie avevano lo stesso shared_ptr sottostante che potresti non aspettarti (modificane uno ed entrambi sono stati modificati). Quindi la soluzione consisteva spesso nell'utilizzare il puntatore non elaborato per quello interno e rendere la classe non copiabile e restituire invece un shared_ptr a quello. Quindi due chiamate a nuovo. (In realtà 3 dato il vecchio shared_ptr te ne ha dato un secondo).

  3. Tecnicamente non è veramente corretto nella const poiché la costanza non viene propagata attraverso un puntatore membro.

In generale, negli anni mi sono quindi allontanato da pImpl e invece ho usato l'interfaccia astratta (e i metodi di fabbrica per creare istanze).


3

Come molti altri hanno detto, l'idioma di Pimpl consente di raggiungere l'indipendenza completa di occultamento e compilazione delle informazioni, sfortunatamente con il costo della perdita di prestazioni (indiretto del puntatore aggiuntivo) e necessità di memoria aggiuntiva (il puntatore del membro stesso). Il costo aggiuntivo può essere critico nello sviluppo di software embedded, in particolare in quegli scenari in cui la memoria deve essere economizzata il più possibile. L'uso delle classi astratte in C ++ come interfacce porterebbe agli stessi benefici allo stesso costo. Ciò mostra in realtà una grande carenza di C ++ in cui, senza ricorrere a interfacce di tipo C (metodi globali con un puntatore opaco come parametro), non è possibile avere una vera indipendenza di occultamento e compilazione di informazioni senza ulteriori svantaggi delle risorse: questo principalmente perché dichiarazione di una classe, che deve essere inclusa dai suoi utenti,


3

Ecco uno scenario reale che ho incontrato, in cui questo idioma ha aiutato moltissimo. Di recente ho deciso di supportare DirectX 11 e il mio supporto DirectX 9 esistente in un motore di gioco. Il motore ha già racchiuso la maggior parte delle funzionalità DX, quindi nessuna delle interfacce DX è stata utilizzata direttamente; sono stati appena definiti nelle intestazioni come membri privati. Il motore utilizza DLL come estensioni, aggiungendo supporto per tastiera, mouse, joystick e script, come settimana come molte altre estensioni. Mentre la maggior parte di quelle DLL non utilizzava DX direttamente, richiedeva conoscenza e collegamento a DX semplicemente perché inseriva le intestazioni che esponevano DX. Aggiungendo DX 11, questa complessità doveva aumentare drammaticamente, anche se inutilmente. Lo spostamento dei membri DX in un Pimpl definito solo nella sorgente ha eliminato questa imposizione. Oltre a questa riduzione delle dipendenze delle biblioteche,


2

È utilizzato in pratica in molti progetti. La sua utilità dipende fortemente dal tipo di progetto. Uno dei progetti più importanti che lo utilizzano è Qt , in cui l'idea di base è nascondere all'utente l'implementazione o il codice specifico della piattaforma (altri sviluppatori che utilizzano Qt).

Questa è una nobile idea, ma presenta un vero svantaggio: debug Finché il codice nascosto nelle implementazioni private è di qualità premium, va bene, ma se ci sono dei bug, l'utente / sviluppatore ha un problema, perché è solo un puntatore stupido a un'implementazione nascosta, anche se ha il codice sorgente delle implementazioni.

Così come in quasi tutte le decisioni di progettazione ci sono pro e contro.


9
è stupido ma è digitato ... perché non riesci a seguire il codice nel debugger?
ZioZeiv,

2
In generale, per eseguire il debug nel codice Qt è necessario creare Qt da soli. Una volta fatto, non c'è problema ad entrare nei metodi PIMPL e ad ispezionare il contenuto dei dati PIMPL.
Ripristina Monica

0

Un vantaggio che posso vedere è che consente al programmatore di eseguire determinate operazioni in modo abbastanza veloce:

X( X && move_semantics_are_cool ) : pImpl(NULL) {
    this->swap(move_semantics_are_cool);
}
X& swap( X& rhs ) {
    std::swap( pImpl, rhs.pImpl );
    return *this;
}
X& operator=( X && move_semantics_are_cool ) {
    return this->swap(move_semantics_are_cool);
}
X& operator=( const X& rhs ) {
    X temporary_copy(rhs);
    return this->swap(temporary_copy);
}

PS: Spero di non fraintendere la semantica delle mosse.

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.