È un buon approccio per una gerarchia di classi basata su "pImpl" in C ++?


9

Ho una gerarchia di classi per la quale vorrei separare l'interfaccia dall'implementazione. La mia soluzione è avere due gerarchie: una gerarchia di classi handle per l'interfaccia e una gerarchia di classi non pubbliche per l'implementazione. La classe handle di base ha un puntatore all'implementazione che le classi di handle derivate eseguono il cast su un puntatore del tipo derivato (vedere funzione getPimpl()).

Ecco uno schizzo della mia soluzione per una classe base con due classi derivate. C'è una soluzione migliore?

File "Base.h":

#include <memory>

class Base {
protected:
    class Impl;
    std::shared_ptr<Impl> pImpl;
    Base(Impl* pImpl) : pImpl{pImpl} {};
    ...
};

class Derived_1 final : public Base {
protected:
    class Impl;
    inline Derived_1* getPimpl() const noexcept {
        return reinterpret_cast<Impl*>(pImpl.get());
    }
public:
    Derived_1(...);
    void func_1(...) const;
    ...
};

class Derived_2 final : public Base {
protected:
    class Impl;
    inline Derived_2* getPimpl() const noexcept {
        return reinterpret_cast<Impl*>(pImpl.get());
    }
public:
    Derived_2(...);
    void func_2(...) const;
    ...
};

File "Base.cpp":

class Base::Impl {
public:
    Impl(...) {...}
    ...
};

class Derived_1::Impl final : public Base::Impl {
public:
    Impl(...) : Base::Impl(...) {...}
    void func_1(...) {...}
    ...
};

class Derived_2::Impl final : public Base::Impl {
public:
    Impl(...) : Base::Impl(...) {...}
    void func_2(...) {...}
    ...
};

Derived_1::Derived_1(...) : Base(new Derived_1::Impl(...)) {...}
Derived_1::func_1(...) const { getPimpl()->func_1(...); }

Derived_2::Derived_2(...) : Base(new Derived_2::Impl(...)) {...}
Derived_2::func_2(...) const { getPimpl()->func_2(...); }

Quale di queste classi sarà visibile dall'esterno della libreria / componente? Se solo Base, una normale classe base astratta ("interfaccia") e implementazioni concrete senza pimpl potrebbero essere sufficienti.
D. Jurcau,

@ D.Jurcau Le classi base e derivate saranno tutte pubblicamente visibili. Ovviamente, le classi di implementazione no.
Steve Emmerson,

Perché abbattere? La classe base si trova in una strana posizione qui, può essere sostituita con un puntatore condiviso con maggiore sicurezza e meno codice.
Basilevs

@Basilevs Non capisco. La classe di base pubblica utilizza l'idioma del pimpl per nascondere l'implementazione. Non vedo come sostituirlo con un puntatore condiviso può mantenere la gerarchia di classi senza lanciare o duplicare il puntatore. Potete fornire un esempio di codice?
Steve Emmerson,

Propongo di duplicare il puntatore, invece di replicare il downcast.
Basilevs

Risposte:


1

Penso che sia una cattiva strategia da cui Derived_1::Implderivare Base::Impl.

Lo scopo principale dell'uso del linguaggio Pimpl è nascondere i dettagli di implementazione di una classe. Lasciando Derived_1::Implderivare Base::Impl, hai sconfitto quello scopo. Ora, non solo la realizzazione di Basedipendere Base::Impl, l'attuazione di Derived_1dipende anche Base::Impl.

C'è una soluzione migliore?

Dipende da quali compromessi sono accettabili per te.

Soluzione 1

Rendi le Impllezioni totalmente indipendenti. Ciò implica che ci saranno due puntatori alle Implclassi: uno dentro Basee l'altro dentro Derived_N.

class Base {

   protected:
      Base() : pImpl{new Impl()} {}

   private:
      // It's own Impl class and pointer.
      class Impl { };
      std::shared_ptr<Impl> pImpl;

};

class Derived_1 final : public Base {
   public:
      Derived_1() : Base(), pImpl{new Impl()} {}
      void func_1() const;
   private:
      // It's own Impl class and pointer.
      class Impl { };
      std::shared_ptr<Impl> pImpl;
};

Soluzione 2

Esporre le classi solo come handle. Non esporre affatto le definizioni e le implementazioni della classe.

File di intestazione pubblico:

struct Handle {unsigned long id;};
struct Derived1_tag {};
struct Derived2_tag {};

Handle constructObject(Derived1_tag tag);
Handle constructObject(Derived2_tag tag);

void deleteObject(Handle h);

void fun(Handle h, Derived1_tag tag);
void bar(Handle h, Derived2_tag tag); 

Ecco una rapida implementazione

#include <map>

class Base
{
   public:
      virtual ~Base() {}
};

class Derived1 : public Base
{
};

class Derived2 : public Base
{
};

namespace Base_Impl
{
   struct CompareHandle
   {
      bool operator()(Handle h1, Handle h2) const
      {
         return (h1.id < h2.id);
      }
   };

   using ObjectMap = std::map<Handle, Base*, CompareHandle>;

   ObjectMap& getObjectMap()
   {
      static ObjectMap theMap;
      return theMap;
   }

   unsigned long getNextID()
   {
      static unsigned id = 0;
      return ++id;
   }

   Handle getHandle(Base* obj)
   {
      auto id = getNextID();
      Handle h{id};
      getObjectMap()[h] = obj;
      return h;
   }

   Base* getObject(Handle h)
   {
      return getObjectMap()[h];
   }

   template <typename Der>
      Der* getObject(Handle h)
      {
         return dynamic_cast<Der*>(getObject(h));
      }
};

using namespace Base_Impl;

Handle constructObject(Derived1_tag tag)
{
   // Construct an object of type Derived1
   Derived1* obj = new Derived1;

   // Get a handle to the object and return it.
   return getHandle(obj);
}

Handle constructObject(Derived2_tag tag)
{
   // Construct an object of type Derived2
   Derived2* obj = new Derived2;

   // Get a handle to the object and return it.
   return getHandle(obj);
}

void deleteObject(Handle h)
{
   // Get a pointer to Base given the Handle.
   //
   Base* obj = getObject(h);

   // Remove it from the map.
   // Delete the object.
   if ( obj != nullptr )
   {
      getObjectMap().erase(h);
      delete obj;
   }
}

void fun(Handle h, Derived1_tag tag)
{
   // Get a pointer to Derived1 given the Handle.
   Derived1* obj = getObject<Derived1>(h);
   if ( obj == nullptr )
   {
      // Problem.
      // Decide how to deal with it.

      return;
   }

   // Use obj
}

void bar(Handle h, Derived2_tag tag)
{
   Derived2* obj = getObject<Derived2>(h);
   if ( obj == nullptr )
   {
      // Problem.
      // Decide how to deal with it.

      return;
   }

   // Use obj
}

Pro e contro

Con il primo approccio, puoi costruire Derivedclassi nello stack. Con il secondo approccio, questa non è un'opzione.

Con il primo approccio, si incorre nel costo di due allocazioni e deallocazioni dinamiche per la costruzione e la distruzione di una Derivedpila. Se costruisci e distruggi un Derivedoggetto dall'heap che hai, incorri nel costo di un'altra allocazione e deallocazione. Con il secondo approccio, dovrai sostenere solo il costo di un'allocazione dinamica e una deallocazione per ogni oggetto.

Con il primo approccio, hai la possibilità di utilizzare la virtualfunzione membro è Base. Con il secondo approccio, questa non è un'opzione.

Il mio consiglio

Vorrei andare con la prima soluzione in modo da poter utilizzare la gerarchia di classi e le virtualfunzioni membro Baseanche se è un po 'più costoso.


0

L'unico miglioramento che posso vedere qui è quello di consentire alle classi concrete di definire il campo di implementazione. Se le classi base astratte ne hanno bisogno, possono definire una proprietà astratta che è facile da implementare nelle classi concrete:

Base.h

class Base {
protected:
    class Impl;
    virtual std::shared_ptr<Impl> getImpl() =0;
    ...
};

class Derived_1 final : public Base {
protected:
    class Impl1;
    std::shared_ptr<Impl1> pImpl
    virtual std::shared_ptr<Base::Impl> getImpl();
public:
    Derived_1(...);
    void func_1(...) const;
    ...
};

Base.cpp

class Base::Impl {
public:
    Impl(...) {...}
    ...
};

class Derived_1::Impl1 final : public Base::Impl {
public:
    Impl(...) : Base::Impl(...) {...}
    void func_1(...) {...}
    ...
};

std::shared_ptr<Base::Impl> Derived_1::getImpl() { return pPimpl; }
Derived_1::Derived_1(...) : pPimpl(std::make_shared<Impl1>(...)) {...}
void Derived_1::func_1(...) const { pPimpl->func_1(...); }

Questo sembra essere più sicuro per me. Se hai un grande albero, puoi anche introdurlo virtual std::shared_ptr<Impl1> getImpl1() =0al centro dell'albero.

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.