It is widely known that std::unique_ptr
may not be conveniently used to implement pimpl idiom: one may not default destructor and move operator right in the header file (e.g., std::unique_ptr with an incomplete type won't compile). Some people suggest using std::shared_ptr
instead, because it uses some trick with destructor that overcomes it (probably just type erasure, but I'm not sure).
I've tried to create a special smart pointer for this case, here's the implementation:
#include <utility>
#include <type_traits>
template <class>
class PimplPtr;
template <class T, class... Args>
PimplPtr<T> MakePimplPtr(Args&&... args);
template <class T>
class PimplPtr {
static_assert(std::is_class_v<T>, "PimplPtr is only intented for use with classes");
template <class S, class... Args>
friend PimplPtr<S> MakePimplPtr(Args&&... args);
public:
PimplPtr() = default;
PimplPtr(const PimplPtr&) = delete;
PimplPtr(PimplPtr&& other) {
ptr_ = other.ptr_;
other.ptr_ = nullptr;
dest_caller_ = other.dest_caller_;
}
PimplPtr& operator=(const PimplPtr&) = delete;
PimplPtr& operator=(PimplPtr&& other) {
Reset();
ptr_ = other.ptr_;
other.ptr_ = nullptr;
dest_caller_ = other.dest_caller_;
}
~PimplPtr() {
Reset();
}
void Reset() {
if (!ptr_) {
return;
}
// first call the destructor
dest_caller_(ptr_);
// then free the memory
operator delete(ptr_);
ptr_ = nullptr;
}
T* operator->() const {
return ptr_;
}
T& operator*() const {
return *ptr_;
}
private:
explicit PimplPtr(T* ptr) noexcept
: ptr_(ptr), dest_caller_(&PimplPtr::DestCaller) {
}
static void DestCaller(T* ptr) {
ptr->~T();
}
using DestCallerT = void (*)(T*);
// pointer to "destructor"
DestCallerT dest_caller_;
T* ptr_{nullptr};
};
template <class T, class... Args>
PimplPtr<T> MakePimplPtr(Args&&... args) {
return PimplPtr{new T(std::forward<Args>(args)...)};
}
Alternatively, one may replace pointer to function with type-erasure, though it will be less efficient, I think.
It works:
class PimplMe {
public:
PimplMe();
// compiles
PimplMe(PimplMe&&) = default;
~PimplMe() = default;
private:
class Impl;
PimplPtr<Impl> impl_;
};
The only downside I see is the little extra overhead involved: one also has to store a pointer to "destructor".
I think that it is not a great deal, because 8-byte overhead is insignificant in pimpl use cases, and my question is of pure interest: is there some practical trick to eliminate space overhead caused by dest_caller_
?
I can think of splitting PimplPtr
into declaration pimpl.hpp
and definition pimpl_impl.hpp
, and explicitly instantiating template PimplPtr<PimplMe::Impl>::Reset()
in impl.cpp
, but I believe it is ugly.
Declaring dest_caller_
as a static member is not a solution, at least because it will require synchronization in multi-threaded case.