Est-ce une bonne approche pour une hiérarchie de classes basée sur «pImpl» en C ++?

9

J'ai une hiérarchie de classes pour laquelle je voudrais séparer l'interface de l'implémentation. Ma solution est d'avoir deux hiérarchies: une hiérarchie de classe de poignée pour l'interface et une hiérarchie de classe non publique pour l'implémentation. La classe de descripteurs de base a un pointeur vers l'implémentation que les classes de descripteurs dérivées convertissent en un pointeur du type dérivé (voir fonction getPimpl()).

Voici un croquis de ma solution pour une classe de base avec deux classes dérivées. Y a-t-il une meilleure solution?

Fichier "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;
    ...
};

Fichier "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(...); }
Steve Emmerson
la source
Laquelle de ces classes sera visible de l'extérieur de la bibliothèque / du composant? Si seulement Base, une classe de base abstraite normale ("interface") et des implémentations concrètes sans pimpl pourraient suffire.
D.Jurcau
@ D.Jurcau Les classes de base et dérivées seront toutes visibles publiquement. De toute évidence, les classes d'implémentation ne le seront pas.
Steve Emmerson
Pourquoi abattu? La classe de base est à une position étrange ici, elle peut être remplacée par un pointeur partagé avec une sécurité de typage améliorée et moins de code.
Basilevs
@Basilevs je ne comprends pas. La classe de base publique utilise l'idiome pimpl pour masquer l'implémentation. Je ne vois pas comment le remplacer par un pointeur partagé peut maintenir la hiérarchie des classes sans transtyper ou dupliquer le pointeur. Pouvez-vous fournir un exemple de code?
Steve Emmerson
Je propose de dupliquer le pointeur, au lieu de répliquer downcast.
Basilevs

Réponses:

1

Je pense que c'est une mauvaise stratégie à faire Derived_1::Impldériver Base::Impl.

L'objectif principal de l'utilisation de l'idiome Pimpl est de masquer les détails d'implémentation d'une classe. En laissant Derived_1::Impldériver Base::Impl, vous avez vaincu cet objectif. Maintenant, non seulement la mise en œuvre de Basedépend Base::Impl, la mise en œuvre de Derived_1dépend également de Base::Impl.

Y a-t-il une meilleure solution?

Cela dépend des compromis que vous acceptez.

Solution 1

Rendez les Implclasses totalement indépendantes. Cela impliquera qu'il y aura deux pointeurs vers les Implclasses - un dans Baseet un autre dans 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;
};

Solution 2

Exposez les classes uniquement en tant que descripteurs. N'exposez pas du tout les définitions de classe et les implémentations.

Fichier d'en-tête public:

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); 

Voici une mise en œuvre rapide

#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
}

Avantages et inconvénients

Avec la première approche, vous pouvez construire des Derivedclasses dans la pile. Avec la deuxième approche, ce n'est pas une option.

Avec la première approche, vous encourez le coût de deux allocations et désallocations dynamiques pour la construction et la destruction d'un Deriveddans la pile. Si vous construisez et détruisez un Derivedobjet à partir du tas vous, encourez le coût d'une allocation et d'une désallocation supplémentaires. Avec la deuxième approche, vous n'encourez que le coût d'une allocation dynamique et d'une désallocation pour chaque objet.

Avec la première approche, vous avez la possibilité d'utiliser la virtualfonction membre est Base. Avec la deuxième approche, ce n'est pas une option.

Ma suggestion

J'irais avec la première solution pour pouvoir utiliser la hiérarchie des classes et virtualles fonctions membres Basemême si c'est un peu plus cher.

R Sahu
la source
0

La seule amélioration que je peux voir ici est de laisser les classes concrètes définir le champ d'implémentation. Si les classes de base abstraites en ont besoin, elles peuvent définir une propriété abstraite facile à implémenter dans les classes concrètes:

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(...); }

Cela me semble plus sûr. Si vous avez un grand arbre, vous pouvez également l'introduire virtual std::shared_ptr<Impl1> getImpl1() =0au milieu de l'arbre.

perruque
la source