1

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.

Nikita Petrenko
  • 1,068
  • 1
  • 7
  • 10
  • 3
    Interesting reading: [pimpl-and-rule-of-zero.html](http://oliora.github.io/2015/12/29/pimpl-and-rule-of-zero.html) – Jarod42 Aug 07 '19 at 23:33
  • 1
    You _can_ (and often should) use a `unique_ptr` for `pimpl` as long as ju declare a destructor in the class carrying the `unique_ptr`. Example: https://www.fluentcpp.com/2017/09/22/make-pimpl-using-unique_ptr/ – Ted Lyngmo Aug 07 '19 at 23:35
  • 1
    @TedLyngmo not really: you cannot default-declare it, which is the whole point of my question – Nikita Petrenko Aug 07 '19 at 23:41
  • 1
    And also [GotW #101: Compilation Firewalls, Part 2](https://herbsutter.com/gotw/_101/) – Jarod42 Aug 07 '19 at 23:42
  • 1
    Yes, you can declare it (`~Class();`), then implement (with `~Class() {}`, or with `~Class() = default;`). once the pimpl is complete. – Ted Lyngmo Aug 07 '19 at 23:43
  • 1
    @TedLyngmo I'm sorry for not being specific in the prev comment. I want to write `~PimplMe() = default` **in the header** – Nikita Petrenko Aug 07 '19 at 23:45
  • 2
    Ok, but I think you are causing more work than it's worth with that requirement. By moving the definition of the dtor into the implementation file, after the pimpl is completely defined, you're all set. – Ted Lyngmo Aug 07 '19 at 23:53

1 Answers1

0

One may not default destructor and move operator right in the header file

Solution is simply to default them right in the source file instead.

While it might not be obvious how to implement PIMPL with unique pointer, it is certainly not impossible, and by writing a re-usable template, the non-obvious parts can be repeated conveniently.

I've written following in the past; I haven't checked if latest standard versions offer a way to simplify it:

// pimpl.hpp (add header guards of your choice)

#include <memory>
template <class T>
class pimpl {
public:
    pimpl(pimpl&&);

    ~pimpl();

    template <class... Args>
    pimpl(Args&&...);

    T* operator->();
    const T* operator->() const;

    T& operator*();
    const T& operator*() const;

private:
    std::unique_ptr<T> m;
};

// pimpl_impl.hpp (add header guards of your choice)
#include <utility>
#include "pimpl.hpp"

template <class T>
pimpl<T>::pimpl(pimpl&&) = default;

template <class T>
pimpl<T>::~pimpl() = default;

template <class T>
template <class... Args>
pimpl<T>::pimpl(Args&&... args) : m{new T{std::forward<Args>(args)...}} {}

template <class T>
T* pimpl<T>::operator->() {
    return m.get();
}

template <class T>
const T* pimpl<T>::operator->() const {
    return m.get();
}

template <class T>
T& pimpl<T>::operator*() {
    return *m.get();
}

template <class T>
const T& pimpl<T>::operator*() const {
    return *m.get();
}

// usage.hpp (add header guards of your choice)
#include "pimpl.hpp"

struct my_class {
    my_class();
    ~my_class();

private:
    pimpl<struct my_impl> m;
};

// usage.cpp
#include "usage.hpp"
#include "pimpl_impl.hpp"

struct my_impl {};

my_class::my_class() = default;
my_class::~my_class() = default;
eerorika
  • 232,697
  • 12
  • 197
  • 326
  • 1
    "Solution is simply to default them right in the source file instead." I am aware of this solution, and find it boilerplate – Nikita Petrenko Aug 07 '19 at 23:53
  • 2
    @NikitaPetrenko Boilerplate? You put one line for the destructor last in the implementation file to make it work. Compare that with your proposal. – Ted Lyngmo Aug 07 '19 at 23:55
  • 1
    @TedLyngmo my proposal is written only once for the whole library, it is asymptotically better :) – Nikita Petrenko Aug 08 '19 at 00:01
  • @TedLyngmo and, by the way, @Jarod42 gave a link to the same idea that I've implemented, but which reuses `std::unique_ptr` – Nikita Petrenko Aug 08 '19 at 00:04
  • @NikitaPetrenko :-) I'll give you that! My own version of what you are doing looked fairly similar and I was also on the verge of dividing it into two files etc. I folded eventually :-) My goal was however to create a `copyable_unique_ptr` that could be used with `pimpl` to be able to use the `default` copy ctor and assignment operator of the carrying class (which `unique_ptr` don't support). – Ted Lyngmo Aug 08 '19 at 00:04
  • @NikitaPetrenko Yeah, the link Jarod42 shared seems to resemble what I've seen before. Good stuff. – Ted Lyngmo Aug 08 '19 at 00:07
  • 1
    @NikitaPetrenko But doesn't your solution instead need boilerplate of calling `MakePimplPtr`? I don't see a reduction of boilerplate. – eerorika Aug 08 '19 at 00:12
  • 1
    @eerorika well, I could add a constructor which would not require it. It is not the point -- what I want is to escape from defining destructors and move operators in the .cpp file. – Nikita Petrenko Aug 08 '19 at 00:23
  • 2
    @NikitaPetrenko You trade off the smallest amount of boilerplate for a runtime penalty? I'm very skeptical of this idea. The pimpl pattern requires a lot of boilerplate anyway. If what you want is to reduce boilerplate, you should skip the pimpl pattern altogether. Otherwise, `MyClass::~MyClass() = default;` is barely any boilerplate at all. You can still define the move operations in the header file; only the destructor needs to be in the cpp file. – Justin Aug 08 '19 at 00:45
  • 1
    @Justin if one uses pimpl for performance critical code, then I have bad news for him. Second, using the approach proposed in the answer leads to cryptic errors like `incomplete type` (though they are compile-time, but it is possible to avoid them) – Nikita Petrenko Aug 08 '19 at 00:50
  • @NikitaPetrenko For someone having implemented pimpl a few times, an error like `incomplete type` sounds familiar and I'd quickly google the resolution, if I didn't remember it right away. – Ted Lyngmo Aug 08 '19 at 01:04
  • 1
    @TedLyngmo maybe I'm a little too keen on perfection, but I believe that the most universal solution which `unique_ptr` provides is far from perfect. And, for such a popular idiom, one should come up with something that is perfectly tailored (if the language permits -- C++ always does, that's why I like it) – Nikita Petrenko Aug 08 '19 at 01:15
  • @NikitaPetrenko I like the ambition for sure! Cheers! – Ted Lyngmo Aug 08 '19 at 01:19