It can be done, with some indirection... Here I come :)
It is based on the implementation of boost::shared_ptr and could benefit from a good speed-up if instead of holding the a pointer to the memory we were actually gluing together the two memory blocks... but then there are alignment issues etc... so I won't do it off the top of my hat.
First, we need a class whose purpose is to manage our memory, with the possibility of supplying a custom deallocator if necessary.
It's indirected, and that's where the magic is.
Notice that it implements a deep-copying behavior.
namespace detail { // The interface template <class T> class MemoryOwnerBase { public: virtual ~MemoryOwnerBase() { this->dispose(mItem); mItem = 0; } virtual void dispose(T* item) = 0; virtual void clone() const = 0; T* get() { return mItem; } T* release() { T* tmp = mItem; mItem = 0; return tmp; } void reset(T* item = 0) { if (mItem && item != mItem) this->dispose(mItem); mItem = item; } protected: explicit MemoryOwnerBase(T* i = 0): mItem(i) {} MemoryOwnerBase(const MemoryOwnerBase& rhs): mItem(0) { if (rhs.mItem) mItem = new_clone(*rhs.mItem); // Boost Clonable concept } MemoryOwnerBase& operator=(const MemoryOwnerBase& rhs) { MemoryOwnerBase tmp(rhs); this->swap(rhs); return *this; } private: T* mItem; }; // by default, call delete template <class T> struct DefaultDisposer { void dispose(T* item) { delete item; } }; // the real class, the type of the disposer is erased from the point of view // of its creator template <class T, class D = DefaultDisposer<T> > class MemoryOwner: public MemoryOwnerBase, private D // EBO { public: MemoryOwner(): MemoryOwnerBase(0), D() {} explicit MemoryOwner(T* item): MemoryOwnerBase(item), D() {} MemoryOwner(T* item, D disposer): MemoryOwnerBase(item), D(disposer) {} virtual void dispose(T* item) { ((D&)*this).dispose(item); } virtual MemoryOwner* clone() const { return new MemoryOwner(*this); } }; // easier with type detection template <class T> MemoryOwnerBase<T>* make_owner(T* item) { return new MemoryOwner<T>(item); } template <class T, class D> MemoryOwnerBase<T>* make_owner(T* item, D d) { return new MemoryOwner<T,D>(item,d); } } // namespace detail
Then we can craft our Pimpl class, since it's what your after.
template <class T> class Pimpl { typedef detail::MemoryOwnerBase<T> owner_base; public: Pimpl(): mItem(0), mOwner(0) {} explicit Pimpl(T* item): mItem(item), mOwner(item == 0 ? 0 : detail::make_owner(item)) {} template <class D> Pimpl(T* item, D d): mItem(item), mOwner(item == 0 ? 0 : detail::make_owner(item, d)) {} Pimpl(const Pimpl& rhs): mItem(), mOwner() { if (rhs.mOwner) { mOwner = rhs.mOwner.clone(); mItem = mOwner->get(); } } T* get() { return mItem; } const T* get() const { return mItem; } void reset(T* item = 0) { if (item && !mOwner) mOwner = detail::make_owner(item); if (mOwner) { mOwner->reset(item); mItem = mOwner->get(); } } template <class D> void reset(T* item, D d) { if (mOwner) { if (mItem == item) mOwner->release(); delete mOwner; } mOwner = detail::make_owner(item, d); mItem = item; } T* operator->() { return mItem; } const T* operator->() const { return mItem; } T& operator*() { return *mItem; } const T& operator*() const { return *mItem; } private: T* mItem; // Proxy for faster memory access detail::MemoryOwnerBase<T>* mOwner; // Memory owner }; // class Pimpl
Okay, pfiou!
Let's use it now :)
// myClass.h class MyClass { public: MyClass(); private: struct Impl; Pimpl<Impl> mImpl; }; // myClass.cpp struct MyClass::Impl { Impl(): mA(0), mB(0) {} int mA; int mB; }; // Choice 1 // Easy MyClass::MyClass(): mImpl(new Impl()) {} // Choice 2 // Special purpose allocator (pool ?) struct MyAllocatorDeleter { void dispose(Impl* item) { /* my own routine */ } }; MyClass::MyClass(): mImpl(new Impl(), MyAllocatorDeleter()) {}
Yes it's magical ;)
The principle behind is call Type Erasure. The mechanism ensures that once the MemoryOwner object is built, it knows how to delete the memory it holds and hide the exact mechanism from the caller through an indirection.
You can thus treat the Pimpl<T> object as a value:
- DeepCopy semantics
- DeepConst semantics (
volatile is ignored...) - Default CopyConstructor, Assignment Operator and Destructor are fine
But beware it hides a pointer and it is your role to make sure it's non-null before dereferencing it.
The code can be greatly simplified if you remove the lazy initialization of the mOwner parameter. Also, some exceptions safety constraints: the disposer copy constructor should be a no-throw, otherwise all bets are off.
EDIT:
Explanations.
The problem here is code insulation. A number of operations can be performed on a pointer regardless of the type pointed to, but for the creation or destruction, we need to know the underlying type.
Creation and Destruction, and thus knowledge about the underlying type, are required in the 4 fundamentals methods:
- Constructor
- Copy Constructor
- Assignment Operator (destruction of the old value)
- Destructor
Which are themselves required to achieve value semantics.
In C++, there is an idiom called type erasure which consists into embedding type information behind a virtual interface. And thus the first part of the design:
template <class T> class MemoryOwnerBase {}; template <class T, class D> class MemoryOwner: public MemoryOwnerBase<T> {};
MemoryOwnerBase provide the basic operations (construction, deep-copying and destruction) we are looking for, and hide type specific information (how to properly delete).
MemoryOwner implements the virtual methods of MemoryOwnerBase and encapsulates the knowledge required for destroying the pointers thanks to its D (for disposer) parameter.
Now, in order to manipulate MemoryOwnerBase we need a pointer / reference to it, which does not have value semantics, and thus we wrap it within the Pimpl class (which stands for pointer-to-implementation) which has proper value semantics.
Note that only the a disposer (for destruction) needs to wrap, since the user is expected to provide a pointer by himself and thus to use the new operator.
A refinement would be to provide a Pimpl<T> make_pimpl<T,D>(const T&, const D&) method that would see to the memory allocation etc... but I haven't yet got to it because of the aforementioned storage alignment issues.