L’idioma pImpl è realmente utilizzato nella pratica?

Sto leggendo il libro “Eccezionale C ++” di Herb Sutter, e in quel libro ho imparato a conoscere l’idioma pImpl. Fondamentalmente, l’idea è di creare una struttura per gli oggetti private di una class e allocarli dynamicmente per ridurre il tempo di compilazione (e anche hide le implementazioni private in un modo migliore).

Per esempio:

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

potrebbe essere cambiato 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 prima questo tipo di approccio, né nelle società in cui ho lavorato, né nei progetti open source che ho visto il codice sorgente. Quindi, mi chiedo se questa tecnica sia realmente utilizzata nella pratica?

Dovrei usarlo ovunque, o con caucanvas? E questa tecnica è raccomandata per essere utilizzata nei sistemi embedded (dove le prestazioni sono molto importanti)?

Quindi, mi chiedo se questa tecnica sia realmente utilizzata nella pratica? Dovrei usarlo ovunque, o con caucanvas?

Ovviamente è usato, e nel mio progetto, in quasi tutte le classi, per diversi motivi che hai menzionato:

  • nascondiglio di dati
  • il tempo di ricompilazione è davvero diminuito, dal momento che solo il file sorgente deve essere ricostruito, ma non l’intestazione e ogni file che lo include
  • compatibilità binaria. Poiché la dichiarazione della class non cambia, è sicuro aggiornare la libreria (presumendo che tu stia creando una libreria)

questa tecnica è raccomandata per l’uso in sistemi embedded (dove le prestazioni sono molto importanti)?

Questo dipende da quanto è potente il tuo objective. Tuttavia, l’unica risposta a questa domanda è: misurare e valutare ciò che guadagni e perdi.

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

Ma come per tutte le cose, non dovresti mai usare nulla ovunque senza caucanvas. 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
  • diminuzione dei cicli di ricompilazione

Quelli possono o non possono essere dei veri vantaggi per te. Per quanto mi riguarda, non mi importa più di qualche minuto di tempo di ricompilazione. Di solito anche gli utenti finali non lo fanno, poiché lo compilano sempre una volta e dall’inizio.

Possibili svantaggi sono (anche qui, a seconda dell’implementazione e se sono per te veri svantaggi):

  • Aumento dell’utilizzo della memoria a causa di più allocazioni rispetto alla variante naïve
  • maggiore sforzo di manutenzione (devi scrivere almeno le funzioni di inoltro)
  • perdita di prestazioni (il compilatore potrebbe non essere in grado di incorporare roba così com’è con un’implementazione ingenua della tua class)

Quindi dai un valore e valuta tutto per te stesso. Per me, quasi sempre risulta che usare l’idioma pimpl non valga la pena. C’è solo un caso in cui lo uso personalmente (o almeno qualcosa di simile):

Il mio wrapper C ++ per la chiamata stat linux. Qui la struttura dell’intestazione C può essere diversa, a seconda di quali #defines sono impostate. E poiché l’intestazione del wrapper non può controllarli tutti, .cxx solo #include nel mio file .cxx ed evito questi problemi.

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

Il motivo è che l’istanziazione del modello richiede la piena dichiarazione disponibile dove l’istanziazione ha avuto luogo. (E questo è il motivo principale per cui non vedi i metodi template definiti in file CPP)

È ancora ansible fare riferimento a sottoclassi templizzate, ma poiché è necessario includerle tutte, tutti i vantaggi del “disaccoppiamento dell’implementazione” durante la compilazione (evitando di includere tutto il codice specifico del platoform ovunque, la compilazione accorciata) vengono persi.

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

Altre persone hanno già fornito gli aspetti tecnici / negativi, ma penso che quanto segue sia degno di nota:

Innanzitutto, non essere dogmatico. Se pImpl funziona per la tua situazione, usalo – non usarlo solo perché “è meglio OO dal momento che nasconde davvero l’ implementazione” ecc. Citando le FAQ C ++:

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

Solo per darti un esempio di software open source dove viene utilizzato e perché: OpenThreads, la libreria di threading utilizzata da OpenSceneGraph . L’idea principale è quella di rimuovere dall’intestazione (ad esempio ) tutto il codice specifico della piattaforma, perché le variabili di stato interne (ad es. I thread handle) differiscono da piattaforma a piattaforma. In questo modo si può compilare un codice contro la propria libreria senza alcuna conoscenza delle idiosincrasie delle altre piattaforms, perché tutto è nascosto.

Prenderò in considerazione PIMPL principalmente per le classi esposte che verranno utilizzate come API da altri moduli. Questo ha molti vantaggi, in quanto rende la ricompilazione delle modifiche apportate nell’implementazione PIMPL non influisce sul resto del progetto. Inoltre, per le classi API promuovono una compatibilità binaria (le modifiche nell’implementazione di un modulo non influenzano i client di quei moduli, non devono essere ricompilate poiché la nuova implementazione ha la stessa interfaccia binaria – l’interfaccia esposta dal PIMPL).

Per quanto riguarda l’uso di PIMPL per ogni class, prenderei in considerazione la caucanvas perché tutti questi benefici hanno un costo: per accedere ai metodi di implementazione è necessario un livello aggiuntivo di riferimento indiretto.

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

Stavo usando pimpl (e molti altri idiomi da Exceptional C ++) sul progetto embedded (SetTopBox).

Lo scopo particolare di questo idoim nel nostro progetto era quello di hide i tipi utilizzati dalla class XImpl. In particolare, lo abbiamo usato per hide i dettagli delle implementazioni per hardware diversi, in cui sarebbero state inserite diverse intestazioni. Avevamo implementazioni diverse delle classi XImpl per una piattaforma e diverse per l’altra. Il layout della class X è rimasto invariato indipendentemente dal platfrom.

Ho usato questa tecnica molto in passato, ma poi mi sono ritrovato ad allontanarmi da esso.

Ovviamente è una buona idea hide i dettagli di implementazione dagli utenti della tua class. Tuttavia puoi farlo anche facendo in modo che gli utenti della class usino un’interfaccia astratta e che i dettagli di implementazione siano la class concreta.

I vantaggi di pImpl sono:

  1. Supponendo che ci sia solo un’implementazione di questa interfaccia, è più chiaro non utilizzando l’astratta class / implementazione concreta

  2. Se hai una suite di classi (un modulo) in modo che più classi accedano allo stesso “impl” ma gli utenti del modulo utilizzeranno solo le classi “esposte”.

  3. Nessun v-table se si presume che sia una cosa negativa.

Gli svantaggi che ho trovato di pImpl (dove l’interfaccia astratta funziona meglio)

  1. Mentre si può avere una sola implementazione di “produzione”, usando un’interfaccia astratta è anche ansible creare un’implementazione “fittizia” che funziona in testing unitario.

  2. (Il più grande problema). Prima dei giorni di unique_ptr e moving hai avuto delle restrizioni su come memorizzare pImpl. Un puntatore non elaborato e hai avuto problemi sul fatto che la tua class non sia copiabile. Un vecchio auto_ptr non funzionerebbe con la class dichiarata in avanti (non su tutti i compilatori in ogni caso). Quindi le persone hanno iniziato a utilizzare shared_ptr, il che è stato utile per rendere la tua copia copiabili, ma ovviamente entrambe le copie avevano lo stesso shared_ptr sottostante che non potresti aspettarti (modificarne una ed entrambe sono state modificate). Quindi la soluzione era spesso usare il puntatore raw per quello interno e rendere la class non-copiabile e restituire invece a shared_ptr. Quindi due chiamate al nuovo. (In realtà 3 dato il vecchio shared_ptr ti ha dato un secondo).

  3. Tecnicamente non proprio costante come la costanza non viene propagata attraverso un puntatore membro.

In generale, mi sono quindi allontanato negli anni da pImpl e in uso di interfacce astratte (e metodi factory per creare istanze).

Come molti altri hanno detto, l’idioma Pimpl consente di raggiungere l’indipendenza completa delle informazioni e della compilazione delle informazioni, sfortunatamente con il costo della perdita di prestazioni (puntatore indiretto aggiuntivo) e la necessità di memoria aggiuntiva (il puntatore membro stesso). Il costo aggiuntivo può essere cruciale nello sviluppo di software embedded, in particolare in quegli scenari in cui la memoria deve essere il più ansible economizzata. Usare le classi astratte C ++ come interfacce porterebbe agli stessi benefici allo stesso costo. Questo mostra in realtà una grande carenza di C ++ dove, senza ricorrere alle interfacce C-like (metodi globali con un puntatore opaco come parametro), non è ansible avere l’indipendenza vera e l’indipendenza di compilazione senza ulteriori inconvenienti di risorse: questo è principalmente dovuto al fatto che il la dichiarazione di una class, che deve essere inclusa dai suoi utenti, esporta non solo l’interfaccia della class (metodi pubblici) necessaria agli utenti, ma anche i suoi interni (membri privati), non necessari agli utenti.

È usato in pratica in molti progetti. La sua utilità dipende fortemente dal tipo di progetto. Uno dei progetti più importanti che utilizzano questo è Qt , in cui l’idea di base è quella di hide l’implementazione o il codice specifico della piattaforma da parte dell’utente (altri sviluppatori che utilizzano Qt).

Questa è una nobile idea, ma c’è un vero svantaggio: debugging Finché il codice nascosto nelle implementazioni private è di qualità premium, questo è tutto a posto, 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.

Quindi, come in quasi tutte le decisioni di progettazione, ci sono pro e contro.

Un vantaggio che posso vedere è che consente al programmatore di implementare 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 del movimento.

Ecco uno scenario reale che ho incontrato, in cui questo idioma ha aiutato molto. Recentemente ho deciso di supportare DirectX 11, oltre al mio supporto DirectX 9, in un motore di gioco. Il motore ha già avvolto la maggior parte delle funzionalità DX, quindi nessuna delle interfacce DX è stata utilizzata direttamente; erano solo definiti nelle intestazioni come membri privati. Il motore utilizza DLL come estensioni, aggiungendo supporto per tastiera, mouse, joystick e scripting, in settimana come molte altre estensioni. Mentre la maggior parte di queste DLL non utilizzava direttamente DX, richiedevano la conoscenza e il collegamento a DX semplicemente perché inserivano 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 librerie, le mie interfacce esposte sono diventate più pulite quando spostate le funzioni dei membri privati ​​nel Pimpl, esponendo solo le interfacce frontali.