Domanda

Mi chiedevo cosa avrebbe indotto un programmatore a scegliere il linguaggio di Pimpl o la pura classe ed eredità virtuale.

Comprendo che l'idioma del pimpl viene fornito con un'indicazione indiretta aggiuntiva esplicita per ciascun metodo pubblico e l'overhead di creazione dell'oggetto.

La classe virtuale pura viene invece con indiretta implicita (vtable) per l'implementazione ereditaria e capisco che nessun sovraccarico di creazione di oggetti.
MODIFICA : ma avresti bisogno di una fabbrica se crei l'oggetto dall'esterno

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

È stato utile?

Soluzione

Quando si scrive una classe C ++, è opportuno pensare se 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 stringa " classe o una "data" classe o un "numero complesso" classe. Per " copia " esempi di tale classe hanno senso.

  2. Un tipo di entità

    L'identità è importante. Passato sempre per riferimento, mai per "valore". Spesso, non ha senso copiare " istanze della classe affatto. Quando ha un senso, un polimero "Clone" il metodo è di solito più appropriato. Esempi: una classe Socket, una classe Database, una " policy " classe, tutto ciò che sarebbe una "chiusura" in un linguaggio funzionale.

Sia pImpl che pura classe base astratta sono tecniche per ridurre le dipendenze dei tempi di compilazione.

Tuttavia, utilizzo sempre pImpl per implementare i tipi di valore (tipo 1), e solo a volte quando voglio davvero minimizzare le dipendenze di accoppiamento e tempo di compilazione. Spesso, non vale la pena. Come giustamente fai notare, c'è più sovraccarico sintattico 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 i metodi factory associati.

Altri suggerimenti

Puntatore all'implementazione solitamente si nasconde i dettagli dell'implementazione strutturale. Interfaces riguarda l'installazione di implementazioni diverse. Servono davvero a due scopi diversi.

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

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

Stavo cercando una risposta per la stessa domanda. Dopo aver letto alcuni articoli e alcune esercitazioni preferisco usare " Interfacce di classe virtuali pure " .

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

L'unico inconveniente ( sto cercando di indagare su questo ) è che il linguaggio del brufolo potrebbe essere più veloce

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

Esiste un problema molto reale con le librerie condivise che il linguaggio del pimpl elude perfettamente che i virtuali puri non possono: non è possibile modificare / rimuovere in sicurezza 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 es. per le librerie di sistema.

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

// 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 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 essere questo .

Sul lato utente del codice, un nuovo A allocerà prima sizeof (A) byte di memoria, quindi passerà un puntatore a quella memoria sul Costruttore A :: A () come this .

Se in una revisione successiva della libreria si decide di eliminare l'intero, renderlo più grande, più piccolo o aggiungere membri, si verificherà una discrepanza tra la quantità di codice dell'utente di memoria allocata e gli offset previsti dal codice del costruttore . Il probabile risultato è un arresto anomalo, se sei fortunato - se sei meno fortunato, il tuo software si comporta in modo strano.

Mediante il pimpl'ing, è possibile aggiungere e rimuovere in modo sicuro i membri dei dati nella classe interna, poiché l'allocazione della memoria e la chiamata del 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'unica ragione 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 dati membri.

Odio i brufoli! Fanno la classe brutta e non leggibile. Tutti i metodi vengono reindirizzati al brufolo. Non si vede mai nelle intestazioni quali funzionalità ha la classe, quindi non è possibile modificarla (ad es. Cambiare semplicemente la visibilità di un metodo). La lezione sembra "incinta". Penso che usare iterfaces sia meglio e abbastanza per nascondere l'implementazione dal client. Puoi consentire a una classe di implementare diverse interfacce per mantenerle sottili. Uno dovrebbe preferire le interfacce! Nota: non è necessaria la classe di fabbrica. Rilevante è che i client di classe comunicano con le relative istanze tramite l'interfaccia appropriata. Il nascondiglio di metodi privati ??lo trovo come una strana paranoia e non vedo motivo per questo dato che abbiamo interfacce.

Non dobbiamo dimenticare che l'eredità è un accoppiamento più forte e più stretto della delega. Vorrei anche tenere conto di tutte le questioni sollevate nelle risposte fornite quando si decide quali idiomi di progettazione utilizzare per risolvere un particolare problema.

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

Un approccio a pimpl è trasparente dal punto di vista dell'utente, il che significa che puoi ad es. creare oggetti della classe nello stack e usarli direttamente in contenitori. Se si tenta di nascondere l'implementazione utilizzando una classe base virtuale astratta, sarà necessario restituire un puntatore condiviso alla classe base da una factory, complicando il suo utilizzo. 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();

Nella mia comprensione queste due cose hanno scopi completamente diversi. Lo scopo del linguaggio del brufolo è fondamentalmente darti una mano per la tua implementazione in modo da poter fare cose come gli swap veloci per una sorta.

Lo scopo delle classi virtuali è più sulla 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 qualunque classe il puntatore di base punti effettivamente a.

Mele e arance davvero.

Il problema più fastidioso del linguaggio del pimpl è che rende estremamente difficile mantenere e analizzare il codice esistente. Quindi usando il pimpl paghi con tempo e frustrazione dello sviluppatore solo per "ridurre dipendenze e tempi di costruzione e minimizzare l'esposizione dell'intestazione dei dettagli di implementazione". Decidi te stesso, se ne vale davvero la pena.

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

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top