Pimpl idioma vs interfaccia di classe virtuale pura


118

Mi chiedevo cosa spingerebbe un programmatore a scegliere l'idioma Pimpl o la pura classe virtuale e l'eredità.

Capisco che l'idioma pimpl viene fornito con un esplicito riferimento indiretto extra per ogni metodo pubblico e il sovraccarico di creazione dell'oggetto.

La classe virtuale Pure d'altra parte viene fornita con l'indirizzamento indiretto implicito (vtable) per l'implementazione ereditaria e capisco che nessun sovraccarico per la creazione di oggetti.
EDIT : Ma avresti bisogno di una fabbrica se crei l'oggetto dall'esterno

Cosa rende la pura classe virtuale meno desiderabile dell'idioma pimpl?


3
Ottima domanda, volevo solo chiedere la stessa cosa. Vedi anche boost.org/doc/libs/1_41_0/libs/smart_ptr/sp_techniques.html
Frank

Risposte:


60

Quando si scrive una classe C ++, è opportuno pensare se lo sarà

  1. Un tipo di valore

    Copia per valore, l'identità non è mai importante. È appropriato che sia una chiave in una std :: map. Esempio, una classe "stringa" o una classe "data" o una classe "numero complesso". "Copiare" istanze di una tale classe ha senso.

  2. Un tipo di entità

    L'identità è importante. Sempre passato per riferimento, mai per "valore". Spesso non ha alcun senso "copiare" istanze della classe. Quando ha senso, un metodo polimorfico "Clone" è solitamente più appropriato. Esempi: una classe Socket, una classe Database, una classe "policy", qualsiasi cosa che sarebbe una "chiusura" in un linguaggio funzionale.

Sia pImpl che la classe base astratta pura sono tecniche per ridurre le dipendenze del tempo di compilazione.

Tuttavia, utilizzo pImpl solo per implementare i tipi di valore (tipo 1) e solo a volte quando voglio davvero ridurre al minimo l'accoppiamento e le dipendenze in fase di compilazione. Spesso non ne vale la pena. Come giustamente fai notare, c'è un sovraccarico sintattico maggiore perché devi scrivere metodi di inoltro per tutti i metodi pubblici. Per le classi di tipo 2, utilizzo sempre una classe base astratta pura con metodi factory associati.


6
Si prega di vedere il commento di Paul de Vrieze a questa risposta . Pimpl e Pure Virtual differiscono in modo significativo se ti trovi in ​​una libreria e desideri scambiare il tuo .so / .dll senza ricostruire il client. I client si collegano ai frontend pimpl per nome, quindi è sufficiente mantenere le vecchie firme dei metodi. OTOH in puro caso astratto si collegano efficacemente tramite indice vtable, quindi il riordino dei metodi o l'inserimento nel mezzo interromperà la compatibilità.
SnakE

1
Puoi solo aggiungere (o riordinare) metodi in un front-end di una classe Pimpl per mantenere la comparabilità binaria. Logicamente parlando, hai ancora cambiato l'interfaccia e sembra un po 'dubbia. La risposta qui è un equilibrio ragionevole che può anche aiutare con i test di unità tramite "iniezione di dipendenza"; ma la risposta dipende sempre dalle esigenze. Gli scrittori di biblioteche di terze parti (a differenza dell'utilizzo di una libreria nella propria organizzazione) potrebbero preferire fortemente il Pimpl.
Spacen Jasset

31

Pointer to implementationdi solito si tratta di nascondere i dettagli di implementazione strutturale. Interfacesriguardano istanze di diverse implementazioni. Servono davvero a due scopi diversi.


13
non necessariamente, ho visto classi che memorizzano più pimpl a seconda dell'implementazione desiderata. Spesso questo è dire un impl win32 contro un impl linux di qualcosa che deve essere implementato in modo diverso per piattaforma.
Doug T.

14
Ma puoi usare un'interfaccia per disaccoppiare i dettagli di implementazione e nasconderli
Arkaitz Jimenez

6
Sebbene sia possibile implementare pimpl utilizzando un'interfaccia, spesso non c'è motivo per disaccoppiare i dettagli dell'implementazione. Quindi non c'è motivo per diventare polimorfici. Il motivo per pimpl è tenere i dettagli di implementazione lontani dal client (in C ++ per tenerli fuori dall'intestazione). Potresti farlo usando una base / interfaccia astratta, ma in genere non è necessario un eccessivo.
Michael Burr

10
Perché è eccessivo? Voglio dire, è più lento il metodo di interfaccia rispetto a quello pimpl? Potrebbero esserci ragioni logiche, ma dal punto di vista pratico direi che è più facile farlo con un'interfaccia astratta
Arkaitz Jimenez

1
Direi che la classe base astratta / interfaccia è il modo "normale" di fare le cose e consente test più facili tramite derisione
paulm

28

L'idioma pimpl ti aiuta a ridurre le dipendenze ei tempi di compilazione, specialmente nelle applicazioni di grandi dimensioni, e riduce al minimo l'esposizione dell'intestazione dei dettagli di implementazione della tua classe a un'unità di compilazione. Gli utenti della tua classe non dovrebbero nemmeno aver bisogno di essere consapevoli dell'esistenza di un brufolo (tranne che come un puntatore criptico di cui non sono a conoscenza!).

Le classi astratte (virtuali puri) sono qualcosa di cui i tuoi clienti devono essere consapevoli: se provi a usarle per ridurre accoppiamenti e riferimenti circolari, devi aggiungere un modo per consentire loro di creare i tuoi oggetti (ad esempio attraverso metodi o classi di fabbrica, iniezione di dipendenza o altri meccanismi).


17

Stavo cercando una risposta per la stessa domanda. Dopo aver letto alcuni articoli e un po 'di pratica, preferisco usare "Interfacce di classe virtuali pure" .

  1. Sono più semplici (questa è un'opinione soggettiva). L'idioma di Pimpl mi fa sentire che sto scrivendo codice "per il compilatore", non per il "prossimo sviluppatore" che leggerà il mio codice.
  2. Alcuni framework di test hanno il supporto diretto per le classi virtuali pure Mocking
  3. È vero che serve una fabbrica per essere accessibile dall'esterno. Ma se vuoi sfruttare il polimorfismo: anche questo è "pro", non "contro". ... e un semplice metodo di fabbrica non fa molto male

L'unico inconveniente ( sto cercando di indagare su questo ) è che l'idioma di pimpl potrebbe essere più veloce

  1. quando le chiamate proxy sono inline, mentre l'ereditarietà richiede necessariamente un accesso extra all'oggetto VTABLE in fase di esecuzione
  2. l'impronta di memoria della classe pimpl public-proxy è più piccola (puoi fare facilmente ottimizzazioni per swap più veloci e altre ottimizzazioni simili)

21
Ricorda anche che usando l'ereditarietà introduci una dipendenza dal layout vtable. Per mantenere l'ABI non è più possibile modificare le funzioni virtuali (aggiungere alla fine è un po 'sicuro, se non ci sono classi figlie che aggiungono metodi virtuali propri).
Paul de Vrieze

1
^ Questo commento qui dovrebbe essere appiccicoso.
CodeAngry

10

Odio i brufoli! Fanno la classe brutta e non leggibile. Tutti i metodi vengono reindirizzati a brufolo. Non si vede mai nelle intestazioni quali funzionalità ha la classe, quindi non è possibile rifattorizzarla (ad esempio cambiare semplicemente la visibilità di un metodo). La classe si sente come "incinta". Penso che l'uso di iterfaces sia migliore e davvero sufficiente per nascondere l'implementazione al client. Puoi lasciare che una classe implementi diverse interfacce per mantenerle sottili. Si dovrebbero preferire le interfacce! Nota: non è necessaria la classe di fabbrica. Rilevante è che i client della classe comunicano con le proprie istanze tramite l'interfaccia appropriata. L'occultamento di metodi privati ​​trovo come una strana paranoia e non vedo il motivo per questo dato che abbiamo interfacce.


1
In alcuni casi non è possibile utilizzare interfacce virtuali pure. Ad esempio, quando hai del codice legacy e hai due moduli che devi separare senza toccarli.
AlexTheo

Come @Paul de Vrieze ha sottolineato di seguito, perdi la compatibilità ABI quando cambi i metodi della classe base, perché hai una dipendenza implicita dalla tabella v della classe. Dipende dal caso d'uso, se questo è un problema.
H. Rittich

"L'occultamento dei metodi privati ​​trovo una strana paranoia" Non ti permette di nascondere le dipendenze e quindi di ridurre al minimo i tempi di compilazione se una dipendenza cambia?
pooya13

Inoltre, non capisco come le fabbriche siano più facili da rifattorizzare rispetto a pImpl. Non lasci "l'interfaccia" in entrambi i casi e modifichi l'implementazione? In Factory devi modificare un file .h e uno .cpp e in pImpl devi modificare un .he due file .cpp ma questo è tutto e generalmente non hai bisogno di modificare il file cpp dell'interfaccia di pImpl.
pooya13

8

C'è un problema molto reale con le librerie condivise che l'idioma pimpl elude ordinatamente che i puri virtuali non possono: non puoi modificare / rimuovere in modo sicuro i membri dei dati di una classe senza costringere gli utenti della classe a ricompilare il loro codice. Ciò può essere accettabile in alcune circostanze, ma non ad esempio per le librerie di sistema.

Per spiegare il problema in dettaglio, considera il seguente codice nella tua libreria / intestazione condivisa:

// header
struct A
{
public:
  A();
  // more public interface, some of which uses the int below
private:
  int a;
};

// library 
A::A()
  : a(0)
{}

Il compilatore emette il codice nella libreria condivisa che calcola l'indirizzo dell'intero da inizializzare per essere un certo offset (probabilmente zero in questo caso, perché è l'unico membro) dal puntatore all'oggetto A che sa di essere this.

Sul lato utente del codice, a new Aprima allocherà sizeof(A)byte di memoria, quindi passerà un puntatore a quella memoria al A::A()costruttore come this.

Se in una revisione successiva della libreria decidi di eliminare il numero intero, renderlo più grande, più piccolo o aggiungere membri, ci sarà una mancata corrispondenza tra la quantità di memoria allocata dal codice dell'utente e gli offset che il codice del costruttore si aspetta. Il probabile risultato è un crash, se sei fortunato - se sei meno fortunato, il tuo software si comporta in modo strano.

Con pimpl'ing, puoi aggiungere e rimuovere in modo sicuro membri di dati alla classe interna, poiché l'allocazione della memoria e la chiamata al costruttore avvengono nella libreria condivisa:

// header
struct A
{
public:
  A();
  // more public interface, all of which delegates to the impl
private:
  void * impl;
};

// library 
A::A()
  : impl(new A_impl())
{}

Tutto quello che devi fare ora è mantenere la tua interfaccia pubblica libera da membri di dati diversi dal puntatore all'oggetto di implementazione e sei al sicuro da questa classe di errori.

Modifica: forse dovrei aggiungere che l'unico motivo per cui sto parlando del costruttore qui è che non volevo fornire più codice: la stessa argomentazione si applica a tutte le funzioni che accedono ai membri dei dati.


4
Invece di void *, penso che sia più tradizionale dichiarare in avanti la classe di implementazione:class A_impl *impl_;
Frank Krueger

9
Non capisco, non dovresti dichiarare membri privati ​​in una classe virtuale pura che intendi usare come interfaccia, l'idea è di mantenere la classe prettamente astratta, nessuna dimensione, solo metodi virtuali puri, non vedo nulla non puoi farlo attraverso le biblioteche condivise
Arkaitz Jimenez

@Frank Krueger: Hai ragione, ero pigro. @Arkaitz Jimenez: Leggero malinteso; se hai una classe che contiene solo funzioni virtuali pure, non ha molto senso parlare di librerie condivise. D'altra parte, se hai a che fare con biblioteche condivise, sfruttare le tue classi pubbliche può essere prudente per il motivo descritto sopra.

10
Questo è semplicemente sbagliato. Entrambi i metodi ti consentono di nascondere lo stato di implementazione delle tue classi, se rendi l'altra classe una classe "base astratta pura".
Paul Hollingsworth,

10
La prima frase nella tua risposta implica che i puri virtuali con un metodo factory associato in qualche modo non ti permettono di nascondere lo stato interno della classe. Non è vero. Entrambe le tecniche consentono di nascondere lo stato interno della classe. La differenza è come appare all'utente. pImpl ti consente di rappresentare ancora una classe con semantica dei valori, ma anche di nascondere lo stato interno. Il metodo Pure Abstract Base Class + factory ti consente di rappresentare i tipi di entità e ti consente anche di nascondere lo stato interno. Quest'ultimo è esattamente come funziona COM. Il capitolo 1 di "Essential COM" ha una grande discussione su questo.
Paul Hollingsworth,

6

Non dobbiamo dimenticare che l'eredità è un legame più forte e più stretto della delega. Vorrei anche prendere in considerazione tutte le questioni sollevate nelle risposte fornite al momento di decidere quali idiomi progettuali utilizzare per risolvere un particolare problema.


3

Sebbene ampiamente trattato nelle altre risposte, forse posso essere un po 'più esplicito su un vantaggio di pimpl rispetto alle classi di base virtuali:

Un approccio pimpl è trasparente dal punto di vista dell'utente, il che significa che puoi ad esempio creare oggetti della classe sullo stack e usarli direttamente nei contenitori. Se provi a nascondere l'implementazione usando una classe base virtuale astratta, dovrai restituire un puntatore condiviso alla classe base da una factory, complicandone l'uso. Considera il seguente codice client equivalente:

// Pimpl
Object pi_obj(10);
std::cout << pi_obj.SomeFun1();

std::vector<Object> objs;
objs.emplace_back(3);
objs.emplace_back(4);
objs.emplace_back(5);
for (auto& o : objs)
    std::cout << o.SomeFun1();

// Abstract Base Class
auto abc_obj = ObjectABC::CreateObject(20);
std::cout << abc_obj->SomeFun1();

std::vector<std::shared_ptr<ObjectABC>> objs2;
objs2.push_back(ObjectABC::CreateObject(13));
objs2.push_back(ObjectABC::CreateObject(14));
objs2.push_back(ObjectABC::CreateObject(15));
for (auto& o : objs2)
    std::cout << o->SomeFun1();

2

Nella mia comprensione queste due cose servono a scopi completamente diversi. Lo scopo dell'idioma brufolo è fondamentalmente darti una guida alla tua implementazione in modo da poter fare cose come scambi veloci per un tipo.

Lo scopo delle classi virtuali è più lungo la linea di consentire il polimorfismo, cioè hai un puntatore sconosciuto a un oggetto di un tipo derivato e quando chiami la funzione x ottieni sempre la funzione giusta per qualsiasi classe a cui punta effettivamente il puntatore di base.

Mele e arance davvero.


Sono d'accordo con le mele / arance. Ma sembra che usi pImpl per un funzionale. Il mio obiettivo è principalmente costruire-tecnico e nascondere le informazioni.
xtofl

2

Il problema più fastidioso dell'idioma pimpl è che rende estremamente difficile mantenere e analizzare il codice esistente. Quindi, utilizzando pimpl si paga con il tempo e la frustrazione dello sviluppatore solo per "ridurre le dipendenze e i tempi di compilazione e ridurre al minimo l'esposizione dell'intestazione dei dettagli di implementazione". Decidi tu stesso se ne vale davvero la pena.

Soprattutto i "tempi di compilazione" sono un problema che puoi risolvere con un hardware migliore o utilizzando strumenti come Incredibuild (www.incredibuild.com, anch'esso già incluso in Visual Studio 2017), senza quindi influire sulla progettazione del software. La progettazione del software dovrebbe essere generalmente indipendente dal modo in cui il software è costruito.


Paghi anche con il tempo dello sviluppatore quando i tempi di costruzione sono di 20 minuti invece di 2, quindi è un po 'un equilibrio, un vero sistema di moduli aiuterebbe molto qui.
Arkaitz Jimenez

Secondo me, il modo in cui il software è costruito non dovrebbe influenzare affatto il design interno. Questo è un problema completamente diverso.
Trantor

2
Cosa rende difficile l'analisi? Un mucchio di chiamate in un file di implementazione inoltrato alla classe Impl non sembra difficile.
mabraham

2
Immagina di eseguire il debug di implementazioni in cui vengono utilizzati sia pimpl che interfacce. Partendo da una chiamata nel codice utente A, si riconduce all'interfaccia B, si passa alla classe pimpled C per avviare finalmente il debug della classe di implementazione D ... Quattro passaggi fino ad analizzare cosa succede realmente. E se l'intera cosa è implementata in una DLL, probabilmente troverai un'interfaccia C da qualche parte tra ....
Trantor

Perché dovresti usare un'interfaccia con pImpl quando pImpl può anche fare il lavoro di un'interfaccia? (cioè può aiutarti a ottenere l'inversione della dipendenza)
pooya13
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.