4

I'm currently researching the pimpl idiom and there are very nice tutorials how it could be implement (e.g. here). But i have never seen it implemented as a base template class like this:

#ifndef PIMPL_H
#define PIMPL_H

template <class T>
class Pimpl
{
public:
    explicit Pimpl();
    explicit Pimpl(T *ptr);
    virtual ~Pimpl() = 0;

    Pimpl(const Pimpl<T> &other);
    Pimpl &operator=(const Pimpl<T> &other);

protected:
    T *d_ptr;
};

template<class T>
Pimpl<T>::Pimpl() : d_ptr(new T)
{

}

template<class T>
Pimpl<T>::Pimpl(T *ptr) : d_ptr(ptr)
{

}

template<class T>
Pimpl<T>::~Pimpl()
{
    delete d_ptr;
    d_ptr = 0;
}

template<class T>
Pimpl<T>::Pimpl(const Pimpl<T> &other) : d_ptr(new T(*other.d_ptr))
{

}

template<class T>
Pimpl<T> &Pimpl<T>::operator=(const Pimpl<T> &other)
{
    if (this != &other) {
        delete d_ptr;
        d_ptr = new T(*other.d_ptr);
    }

    return *this;
}

#endif // PIMPL_H

Which then could be used in any class you like to pimpl:

#ifndef OBJECT_H
#define OBJECT_H

#include "pimpl.h"

class ObjectPrivate;

class Object : public Pimpl<ObjectPrivate>
{
public:
    Object();
    virtual ~Object();

    /* ... */
};

#endif // OBJECT_H

Currently i'm using it in a small example project (build as a shared library) and the only problem i had, was that MSVC warns about the missing destructor for ObjectPrivate (see C4150). This warning only occurs, because ObjectPrivate is forward declared and therefore not visible to the delete operator in Pimpl::~Pimpl() at compile time.

Does anyone see any sort of problems with this approach? :-)


So there is a now a final version based on the discussion below on GitHub (big thanks to StoryTeller). The repository also contains a simple usage example.

0x2648
  • 83
  • 8
  • 2
    [Avoid identifiers that begin with a double leading underscore](http://eel.is/c++draft/lex.name#3). A single underscore in class scope is okay. – StoryTeller - Unslander Monica May 22 '17 at 09:30
  • Thanks for the hint! Answer and example is up to date. – 0x2648 May 22 '17 at 11:00
  • I should also note that your switch to a static cast changes semantics a bit. The live example I provided uses private inheritance (perfectly valid for a mixin, IMO). [A static cast will fail, while a c-style cast won't](http://en.cppreference.com/w/cpp/language/explicit_cast). It's the one valid use of a c-style cast. The way you have it, a user of your class will need to add boilerplate that declares the `Pimpl` as a friend. – StoryTeller - Unslander Monica May 22 '17 at 11:20
  • So currently i'm running into serveral problems with the code from your live example while compiling with **MinGW 5.3.0 32bit**. I put everything in separated files (pimpl.h, object_p.h, object.h/.cpp, main.cpp). If i compile with private inheritance, `unmake` and `clone` are inaccessible within `_unmake` and `_clone`. If i compile with public inheritance, the compiler complains about the invalid use of the incomplete type "class ObjectPrivate" within `clone`. It also warns about that `p` within `unmake` has a incomplete type. – 0x2648 May 22 '17 at 12:21
  • I already addresses the accessibility issue in my previous comment. And about the incomplete type, refer to the first paragraph after the example in my post. Put the definitions of the Object c'tor, d'tor and assignment operator in `object.cpp` only. They cannot be inline – StoryTeller - Unslander Monica May 22 '17 at 12:28
  • For an easier discussion, i've added the example code on [github](https://github.com/0x2648/pimpl-template). The current version uses the pimpl version from your example (and should also follow everthing you mentioned). I'm not able to compile with same errors as above. – 0x2648 May 22 '17 at 13:30
  • 1
    Can you humor me and replace the pimpl template with `std::unique_ptr`? Or test your project with GCC and/or clang? I strongly suspect it's a problem with MinGW. Members of templates aren't instantiated until used (according to the C++ standard). Which is why the guidelines about implementation. Your project is okay at a glance. – StoryTeller - Unslander Monica May 22 '17 at 14:00
  • 1
    Using `std::unique_ptr d_ptr` instead of inherting `Pimpl` works fine (if you don't mind that Object can't be copied anymore). I could only test the default implementation with MSVC 2015 which behaves exactly like MinGW. – 0x2648 May 22 '17 at 14:40
  • Well, I took a long hard look at it again. Your `Object` class doesn't declare an `operator=`, hence the compiler generates one inline. Therefore the definition of `Pimpl::operator=` is instantiated and called, which results in clone attempting to copy an incomplete type. So that project didn't heed my [original comment](https://stackoverflow.com/questions/43913849/pimpl-idiom-as-template-base-class/43914535#comment75245017_43913849) – StoryTeller - Unslander Monica May 22 '17 at 18:29
  • **Right direction!** The error was caused by the missing copy constructor in Object (see commit [5f4310c](https://github.com/0x2648/pimpl-template/commit/5f4310c2a945b08b161dd69d5cfd95858e7c1de7)). Now everything compiles without an error/warning. – 0x2648 May 22 '17 at 20:45

3 Answers3

5

Yes, there are several problems, as I see it.

  1. Your class is essentially a mixin. It's not about dynamic polymorphism, so no-one is ever going to call delete on a pointer to Pimpl<ObjectPrivate>. Drop the virtual destructor. It's introducing overhead that's never going to be required. What you want is static polymorphism only.

  2. You allocate the object with new and release with delete. I won't use your template, because that allocation scheme isn't always appropriate in my applications. You must give a way to customize the allocation scheme in order to make your class actually useful.

  3. Your assignment operator isn't exception safe. If the constructor for T throws, you lose the previously saved data. IMO it's better in this case to use the copy and swap idiom.

The solution to (1) and (2) is to add more template parameters, where the first is for the CRTP. This will allow you to push operations you aren't aware of how to do, onto the class that inherits your mixin. It can override them by defining its own make, unmake and clone. And those will all be bound statically.

template <class Handle, class Impl>
class Pimpl
{
private:
    Impl* _make() const
    { return ((Handle const*)this)->make(); }

    void _unmake(Impl *p) const
    { ((Handle const*)this)->unmake(p); }

    Impl* _clone(Impl *p) const
    { return ((Handle const*)this)->clone(p); }

    void swap(Pimpl &other) {
        Impl *temp = d_ptr;
        d_ptr = other.d_ptr;
        other.d_ptr = temp;
    }

public:
    explicit Pimpl();
            ~Pimpl();

    Pimpl(const Pimpl &other);
    Pimpl &operator=(const Pimpl &other);

    // fall-backs
    static Impl* make()          { return new Impl; }
    static void  unmake(Impl* p) { delete p; }
    static Impl* clone(Impl* p)  { return new Impl(*p); }

protected:

    Impl *d_ptr;
};

template<class Handle, class Impl>
Pimpl<Handle, Impl>::Pimpl() :
  d_ptr(_make())
{

}

template<class Handle, class Impl>
Pimpl<Handle, Impl>::~Pimpl()
{
    _unmake(d_ptr);
    d_ptr = 0;
}

template<class Handle, class Impl>
Pimpl<Handle, Impl>::Pimpl(const Pimpl &other) :
  d_ptr(_clone(other.d_ptr))
{

}

template<class Handle, class Impl>
Pimpl<Handle, Impl> &Pimpl<Handle, Impl>::operator=(const Pimpl &other)
{
    Pimpl copy(other);
    swap(copy);

    return *this;
}

Live Example

Now your header can compile cleanly. So long as the destructor for Object isn't defined inline. When it's inline the compiler must instantiate the destructor of the template wherever object.h is included.

If it's defined in a cpp file, after the definition of ObjectPrivate, then the instantiation of ~Pimpl will see the full definition of the private parts.

Further ideas for improvement:

  1. Make the special members protected. It's only the derived Handle class that's supposed to call them, after all.

  2. Add support for move semantics.

StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
  • I like you answer a lot (and sorry for the long response time, i had to read through some of things you mentioned). The only problem is see is, that with `make()` it's only possible to call one constructor (default or specialised) to create `ObjectPrivate`. I had the same problem earlier and solved it by adding `Pimpl(T *ptr)` to set the `d_ptr` directly. Also by implementing the copy constructor in `ObjectPrivate` you could control the behavior of `new T(*other.d_ptr)`. So do you really need `make()` and `clone()`? – 0x2648 May 18 '17 at 16:04
  • @0x2648 - Letting the user set the pointer is messy in my opinion. Since your destructor always calls delete, the user must concern himself with your implementation details. That's why I introduced those functions to begin with. The static `make`/`clone`/`unmake` are only fallbacks for when the inherited class doesn't want to do anything better than new/delete. They can be **statically overridden** (See example [here](http://coliru.stacked-crooked.com/a/fed79d1713dd1dc0)) to support any allocation scheme. The don't even need to be static. – StoryTeller - Unslander Monica May 18 '17 at 21:09
  • I see your point. So i will accept your post as answer and add a finale implementation to my question. :-) – 0x2648 May 19 '17 at 08:48
2

But i have never seen it implemented as a base template class

Vladimir Batov did it: https://github.com/yet-another-user/pimpl

Does anyone see any sort of problems with this approach?

You need to take the warning seriously. If your ObjectPrivate actually has a non-trivial destructor (which is as easy as containing a std::string member), you have undefined behavior, and the destructor probably won't get called.

This typically suggests that for some reason, the destructor is instantiated in the wrong place. Make sure that all definitions of all constructors and destructors of the derived class are placed after the full definition of ObjectPrivate. This includes the implicit copy and move constructors, which are probably what triggers the warning in your example code. Yes, this means you have to explicitly declare these special functions (and as a consequence, also the copy and move assignment operators if you want them), but at least you can use a defaulted definition.

I don't know if Vlad's library has the same problem.

Also, nulling out pointers in a destructor is pointless and will probably just get optimized away by some modern compilers.

Sebastian Redl
  • 69,373
  • 8
  • 123
  • 157
1

Modern version that I'm using:


///////////////////////////////
// Header File
template <typename impl_t>
class Pimpl {
  public:
    Pimpl() = default;
    virtual ~Pimpl() = default;
    Pimpl(std::shared_ptr<impl_t> handle) : handle(handle) {}

    std::shared_ptr<impl_t>
    get_handle() const {
      return handle;
    }
  protected:
    std::shared_ptr<impl_t> handle;
};

class object_impl;
class object : public Pimpl<object_impl> {
/* whatever constructors you want*/
public:
  object(int x);
}

///////////////////////////////
// Cpp File
class object_impl {
public:
  object_impl(int x) : x_(x) {}
private:
  int x_;
}

object::object(int x) : Pimpl(std::make_shared<object_impl>(x)) {}
CraigDavid
  • 1,046
  • 1
  • 12
  • 26