Domanda

Di recente ho riacceso da Java e Ruby in C ++, e con mia grande sorpresa devo ricompilare i file che utilizzano l'interfaccia pubblica quando cambio la firma del metodo di un metodo privato, perché anche le parti intime sono in .h file.

Ho subito si avvicinò con una soluzione che è, credo, tipico per un programmatore Java: interfacce (= classi base virtuali puri). Ad esempio:

BananaTree.h:

class Banana;

class BananaTree
{
public:
  virtual Banana* getBanana(std::string const& name) = 0;

  static BananaTree* create(std::string const& name);
};

BananaTree.cpp:

class BananaTreeImpl : public BananaTree
{
private:
  string name;

  Banana* findBanana(string const& name)
  {
    return //obtain banana, somehow;
  }

public:
  BananaTreeImpl(string name) 
    : name(name)
  {}

  virtual Banana* getBanana(string const& name)
  {
    return findBanana(name);
  }
};

BananaTree* BananaTree::create(string const& name)
{
  return new BananaTreeImpl(name);
}

L'unica seccatura qui, è che non posso usare new, e devo invece chiamare BananaTree::create(). Non credo che questo è davvero un problema, soprattutto perché mi aspetto di essere utilizzando le fabbriche molto comunque.

Ora, i saggi di C ++ fama, tuttavia, si avvicinò con un'altra soluzione, il Pimpl . Con questo, se ho ben capito, il mio codice sarà simile:

BananaTree.h:

class BananaTree
{
public:
  Banana* addStep(std::string const& name);

private:
  struct Impl;
  shared_ptr<Impl> pimpl_;
};

BananaTree.cpp:

struct BananaTree::Impl
{
  string name;

  Banana* findBanana(string const& name)
  {
    return //obtain banana, somehow;
  }

  Banana* getBanana(string const& name)
  {
    return findBanana(name);
  }

  Impl(string const& name) : name(name) {}
}

BananaTree::BananaTree(string const& name)
  : pimpl_(shared_ptr<Impl>(new Impl(name)))
{}

Banana* BananaTree::getBanana(string const& name)
{
  return pimpl_->getBanana(name);
}

Ciò significa che devo implementare un metodo di inoltro decoratore-stile per ogni metodo pubblico di BananaTree, in questo caso getBanana. Questo suona come un ulteriore livello di complessità e di manutenzione sforzo che preferisco non richiedere.

Quindi, ora la domanda: Che cosa è sbagliato con il puro approccio classe virtuale? Perché l'approccio Pimpl molto meglio documentata? Mi sono perso qualcosa?

È stato utile?

Soluzione

mi viene in mente alcune differenze:

Con la classe base virtuale si rompe alcune delle semantiche la gente si aspetta da ben educati classi C ++:

mi aspetto (o richiedere, anche) la classe da un'istanza sullo stack, in questo modo:

BananaTree myTree("somename");

In caso contrario, perdo Raii, e devo avviare manualmente il monitoraggio allocazioni, che porta ad un sacco di mal di testa e perdite di memoria.

Mi aspetto inoltre che per copiare la classe, posso semplicemente fare questo

BananaTree tree2 = mytree;

a meno che, naturalmente, la copia non è consentita contrassegnando il costruttore di copia privata, in questo caso quella linea non sarà nemmeno la compilazione.

Nei casi di cui sopra, abbiamo ovviamente hanno il problema che la classe di interfaccia in realtà non hanno costruttori significativi. Ma se ho cercato di utilizzare il codice, come gli esempi sopra riportati, mi piacerebbe anche conciliarsi con un sacco di problemi per affettare. Con gli oggetti polimorfici, si sta generalmente richiesto di puntatori attesa o riferimenti agli oggetti, per evitare affettare. Come nel mio primo punto, questo non è in genere desiderabile, e rende la gestione della memoria molto più difficile.

Sarà un lettore del codice capire che un BananaTree in fondo non funziona, che deve usare BananaTree* o BananaTree& invece?

In sostanza, l'interfaccia semplicemente non gioca molto bene con i moderni C ++, dove preferiamo

  • puntatori evitare il più possibile, e
  • pila-allocare tutti gli oggetti a forma di beneficio gestione automatica vita.

Tra l'altro, la classe base virtuale dimenticato il distruttore virtuale. Questo è un chiaro errore.

Infine, una variante più semplice di Pimpl che a volte io uso per ridurre la quantità di codice boilerplate è quello di dare l'accesso agli oggetti "esterno" per i membri di dati dell'oggetto interno, in modo da evitare la duplicazione l'interfaccia. Sia una funzione sull'oggetto esterno appena accede ai dati necessari dal oggetto interno direttamente, oppure chiama una funzione di supporto sull'oggetto interna, che non ha equivalenti sull'oggetto esterno.

Nel tuo esempio, è possibile rimuovere la funzione e Impl::getBanana, e invece implementare BananaTree::getBanana in questo modo:

Banana* BananaTree::getBanana(string const& name)
{
  return pimpl_->findBanana(name);
}

allora si hanno solo per implementare una funzione getBanana (nella classe BananaTree), e una funzione findBanana (nella classe Impl).

Altri suggerimenti

In realtà, questa è solo una decisione di progettazione per fare. E anche se si effettua la decisione "sbagliata", non che è difficile da passare.

Pimpl è utilizzato anche per fornire oggetti ligthweight sulla pila o di presentare "copia" facendo riferimento allo stesso oggetto attuazione.
La delegazione funzioni può essere una seccatura, ma è un problema minore (semplice, la complessità in modo che nessun aggiunto reale), in particolare con le classi limitate.

interfacce in C ++ sono in genere più utilizzato nella strategia simile modi in cui ci si aspetta di essere in grado di scegliere le implementazioni, anche se questo non è richiesto.

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