Ist dies ein guter Ansatz für eine "pImpl" -basierte Klassenhierarchie in C ++?


9

Ich habe eine Klassenhierarchie, für die ich die Schnittstelle von der Implementierung trennen möchte. Meine Lösung besteht darin, zwei Hierarchien zu haben: eine Handle-Klassenhierarchie für die Schnittstelle und eine nicht öffentliche Klassenhierarchie für die Implementierung. Die Basis-Handle-Klasse verfügt über einen Zeiger auf die Implementierung, den die abgeleiteten Handle-Klassen in einen Zeiger des abgeleiteten Typs umwandeln (siehe Funktion getPimpl()).

Hier ist eine Skizze meiner Lösung für eine Basisklasse mit zwei abgeleiteten Klassen. Gibt es eine bessere Lösung?

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

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

Welche dieser Klassen ist von außerhalb der Bibliothek / Komponente sichtbar? Wenn nur Base, könnten eine normale abstrakte Basisklasse ("Schnittstelle") und konkrete Implementierungen ohne Pimpl ausreichen.
D. Jurcau

@ D.Jurcau Die Basis- und abgeleiteten Klassen sind alle öffentlich sichtbar. Offensichtlich werden die Implementierungsklassen dies nicht tun.
Steve Emmerson

Warum niedergeschlagen? Die Basisklasse befindet sich hier an einer seltsamen Position. Sie kann durch einen gemeinsam genutzten Zeiger mit verbesserter Typensicherheit und weniger Code ersetzt werden.
Basilevs

@ Basilevs verstehe ich nicht. Die öffentliche Basisklasse verwendet die Pimpl-Sprache, um die Implementierung auszublenden. Ich sehe nicht, wie das Ersetzen durch einen gemeinsam genutzten Zeiger die Klassenhierarchie beibehalten kann, ohne den Zeiger umzuwandeln oder zu duplizieren. Können Sie ein Codebeispiel angeben?
Steve Emmerson

Ich schlage vor, den Zeiger zu duplizieren, anstatt den Downcast zu replizieren.
Basilevs

Antworten:


1

Ich denke, es ist eine schlechte Strategie, Derived_1::Impldaraus abzuleiten Base::Impl.

Der Hauptzweck der Verwendung des Pimpl-Idioms besteht darin, die Implementierungsdetails einer Klasse auszublenden. Indem Sie Derived_1::Implableiten lassen Base::Impl, haben Sie diesen Zweck besiegt. Nun, nicht nur , dass die Umsetzung Basehängt davon ab Base::Impl, die Umsetzung der Derived_1auch abhängig von Base::Impl.

Gibt es eine bessere Lösung?

Das hängt davon ab, welche Kompromisse für Sie akzeptabel sind.

Lösung 1

Machen Sie den ImplUnterricht völlig unabhängig. Dies bedeutet, dass es zwei Zeiger auf ImplKlassen gibt - einen in Baseund einen in 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;
};

Lösung 2

Stellen Sie die Klassen nur als Handles zur Verfügung. Machen Sie die Klassendefinitionen und Implementierungen überhaupt nicht verfügbar.

Öffentliche Header-Datei:

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

Hier ist eine schnelle Implementierung

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

Vor-und Nachteile

Mit dem ersten Ansatz können Sie DerivedKlassen im Stapel erstellen. Beim zweiten Ansatz ist dies keine Option.

Beim ersten Ansatz fallen die Kosten für zwei dynamische Zuweisungen und Freigaben für die Erstellung und Zerstörung eines DerivedStacks an. Wenn Sie ein DerivedObjekt aus dem Heap erstellen und zerstören , entstehen Ihnen die Kosten für eine weitere Zuordnung und Freigabe. Beim zweiten Ansatz fallen nur die Kosten für eine dynamische Zuordnung und eine Freigabe für jedes Objekt an.

Mit dem ersten Ansatz erhalten Sie die Möglichkeit, die virtualMitgliedsfunktion zu nutzen Base. Beim zweiten Ansatz ist dies keine Option.

Mein Vorschlag

Ich würde mich für die erste Lösung entscheiden, damit ich die Klassenhierarchie und die virtualElementfunktionen verwenden kann Base, obwohl sie etwas teurer sind.


0

Die einzige Verbesserung, die ich hier sehen kann, besteht darin, dass die konkreten Klassen das Implementierungsfeld definieren. Wenn die abstrakten Basisklassen dies benötigen, können sie eine abstrakte Eigenschaft definieren, die in den konkreten Klassen einfach zu implementieren ist:

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

Das scheint mir sicherer zu sein. Wenn Sie einen großen Baum haben, können Sie ihn auch virtual std::shared_ptr<Impl1> getImpl1() =0in der Mitte des Baumes einführen .

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.