Je pense que c'est une mauvaise stratégie à faire Derived_1::Impl
dé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::Impl
dériver Base::Impl
, vous avez vaincu cet objectif. Maintenant, non seulement la mise en œuvre de Base
dépend Base::Impl
, la mise en œuvre de Derived_1
dépend également de Base::Impl
.
Y a-t-il une meilleure solution?
Cela dépend des compromis que vous acceptez.
Solution 1
Rendez les Impl
classes totalement indépendantes. Cela impliquera qu'il y aura deux pointeurs vers les Impl
classes - un dans Base
et 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 Derived
classes 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 Derived
dans la pile. Si vous construisez et détruisez un Derived
objet à 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 virtual
fonction 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 virtual
les fonctions membres Base
même si c'est un peu plus cher.
Base
, une classe de base abstraite normale ("interface") et des implémentations concrètes sans pimpl pourraient suffire.