9

I have a class hierarchy for which I would like to separate the interface from the implementation. My solution is to have two hierarchies: a handle class hierarchy for the interface and a non-public class hierarchy for the implementation. The base handle class has a pointer-to-implementation which the derived handle classes cast to a pointer of the derived type (see function getPimpl()).

Here's a sketch of my solution for a base class with two derived classes. Is there a better solution?

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(...); }
  • Which of these classes will be visible from the outside of the library/component? If only Base, a normal abstract base class ("interface") and concrete implementations without pimpl might be enough. – D. Jurcau Jan 12 '17 at 19:18
  • @D.Jurcau The base and derived classes will all be publicly visible. Obviously, the implementation classes will not. – Steve Emmerson Jan 12 '17 at 21:09
  • Why downcast? Base class is at strange position here, it can be replaced with a shared pointer with improved typeasafety and less code. – Basilevs Jan 13 '17 at 1:19
  • @Basilevs I don't understand. The public base class uses the pimpl idiom to hide the implementation. I don't see how replacing it with a shared pointer can maintain the class hierarchy without casting or duplicating the pointer. Can you provide a code example? – Steve Emmerson Jan 13 '17 at 1:51
  • I propose to duplicate pointer, instead of replicating downcast. – Basilevs Jan 13 '17 at 1:57
1

I think it is a poor strategy to make Derived_1::Impl derive from Base::Impl.

The main purpose of using the Pimpl idiom is to hide the implementation details of a class. By letting Derived_1::Impl derive from Base::Impl, you've defeated that purpose. Now, not only does the implementation of Base depend on Base::Impl, the implementation of Derived_1 also depends on Base::Impl.

Is there a better solution?

That depends on what trade-offs are acceptable to you.

Solution 1

Make Impl classes totally independent. This will imply that there will be two pointers to Impl classes -- one in Base and another one 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;
};

Solution 2

Expose the classes only as handles. Don't expose the class definitions and implementations at all.

Public header file:

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

Here's quick implementation

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

Pros and Cons

With the first approach, you can construct Derived classes in the stack. With the second approach, that is not an option.

With the first approach, you incur the cost of two dynamic allocations and deallocations for constructing and destructing a Derived in the stack. If you construct and destruct a Derived object from the heap you, incur the cost of one more allocation and deallocation. With the second approach, you only incur the cost of one dynamic allocation and one deallocation for every object.

With the first approach, you get have the ability to use virtual member function is Base. With the second approach, that is not an option.

My suggestion

I would go with the first solution so I can use the class hierarchy and virtual member functions in Base even though it is a little bit more expensive.

0

The only improvement I can see here is to let the concrete classes define the implementation field. If the abstract base classes need it, they can define an abstract property that is easy to implement in the concrete classes:

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

This seems to be safer to me. If you have a big tree, you can also introduce virtual std::shared_ptr<Impl1> getImpl1() =0 in the middle of the tree.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.