1

I'm trying to define existing code that use "pimpl" data members to be defined with unique_ptr. Some objects requires a custom deleter, and others not.

unique_ptr (unlike shared_ptr) destructor requires knowing a complete type of the object. So you need to specify the deleter in the data member declaration:

class Foo {
public:
   ...
   ~Foo (void) //in source file  =default
private:
   class FooImpl;
   std::unique_ptr <FooImpl> _pimpl;
};

When instantiating the pimpl you are constrained to using the default deleter. If you want a custom deleter, you need to specify it in the declaration

class Foo {
public:
   ...
   ~Foo (void) //in source file  =default
private:
   class FooImpl;
   std::unique_ptr <FooImpl,  std::function <void (FooImpl*&)> > _pimpl;
};

However, you can't have the option to be flexible whether you want the unique_ptr d'tor with default behaviour or a custom deleter. The more flexible option is the second version, but if you choose to keep with the default behaviour, then you must instantiate the unique_ptr with a specific deleter that is equivalent to the default delete, such as:

Foo::Foo (void) :
    _impl (new CurveItemWidgetImpl, std::default_delete <FooImpl> ()) {
}

So, is std::unique_ptr the best way to handle ABI (compared to shared_ptr or raw pointer)?

Daniel Heilper
  • 1,182
  • 2
  • 17
  • 34
  • 1
    Don't use `std::function` as `unique_ptr`'s deleter. Its constructor can throw. – T.C. May 06 '15 at 09:10
  • @T.C so can you give another way to generalize the deleter? – Daniel Heilper May 06 '15 at 09:12
  • *Why* would you want anything other than `default_delete` as the `pImpl` deleter? – ecatmur May 06 '15 at 09:16
  • @ecatmur there are examples that the deleter may be custom even for the pImpl, for instance if the object should not be deleted (null deleter) during the programs life-time, or if you want to have some generic logging of the destruction. – Daniel Heilper May 06 '15 at 09:27
  • if you want custom control of the deletion process coupled with a pimpl idiom, you're probably better off just storing a `FooImpl*` and implementing the 5 custom destructor, assignments and copies. – Richard Hodges May 06 '15 at 09:55
  • @Richard Hodges but RAII should be preferred on raw pointer (e.g exception handling) – Daniel Heilper May 06 '15 at 10:02
  • I agree, but you're going to be handling lifetime of the impl anyway in your custom deleter, so why not take full responsibility for it? – Richard Hodges May 06 '15 at 10:13

2 Answers2

3

You can easily supply a deleter that effectively calls an opaque symbol:

class Foo {
public:
  ~Foo(); // = default
private:
  class FooImpl;
  struct FooImplDelete { void operator()(FooImpl*); };
  std::unique_ptr<FooImpl, FooImplDelete> _pimpl;
};

Now you can move the definition of Foo::FooImplDelete::operator() to your source file. Indeed, an optimizing compiler will inline it into the destructor of Foo.

If you have no particular reason to suspect that a custom deleter will be required, you might as well use the default; should you ever need a custom deleter change to release the unique_ptr:

Foo::Foo() try
  : _pimpl(new FooImpl)
{
}
catch(...)
{
  delete _pimpl.release();
}

Foo::~Foo()
{
  delete _pimpl.release();
}
ecatmur
  • 152,476
  • 27
  • 293
  • 366
  • Very nice. Further, I would like to construct a RAII that enforce a custom deleter option to all of my _pImpl's. So I need to define a new RAII with Deleter's operator () would take void* parameter (or maybe better using decltype) . Could you make further enhancement? – Daniel Heilper May 06 '15 at 10:18
  • @ecatmur I stand corrected - will withdraw prior comments – Richard Hodges May 06 '15 at 11:08
2

Here are 2 ways that are compliant and portable.

This first is to simply manage the memory yourself (you'll see that it's trivial). The second leverages the unique_ptr

Self managed:

class Foo 
{
public:
    Foo();

    Foo(Foo&& rhs) noexcept;
    Foo(const Foo&) = delete;
    Foo& operator=(Foo&& rhs) noexcept;
    Foo& operator=(const Foo&) = delete;
    ~Foo() noexcept;
private:
    class impl;
    impl* _pimpl = nullptr;
};

// implementation:

class Foo::impl
{
    // TODO: implement your class here
};

// example constructor
Foo::Foo()
: _pimpl { new impl {} }
{}

Foo::Foo(Foo&& rhs) noexcept
: _pimpl { rhs._pimpl }
{
    rhs._pimpl = nullptr;
}

Foo& Foo::operator=(Foo&& rhs) noexcept
{
    using std::swap;
    swap(_pimpl, rhs._pimpl);
}

Foo::~Foo() noexcept
{
    // perform custom actions here, like moving impl onto a queue

    // or just delete it
    delete _pimpl;
}

using unique_ptr:

Foo.h:

#ifndef __included__foo__h__
#define __included__foo__h__

#include <memory>

class Foo 
{
public:
    Foo();
    ~Foo();
private:
    class impl;
    std::unique_ptr<impl, void (*) (impl*) noexcept> _pimpl;
    static void handle_delete(impl*) noexcept;
};


#endif

Foo.cpp:

#include "Foo.h"

class Foo::impl
{
    // TODO: implement your class here
};

// example constructor
Foo::Foo()
: _pimpl { new impl, &Foo::handle_delete }
{

}

Foo::~Foo() = default;

void Foo::handle_delete(impl* p) noexcept
{
    // do whatever you need to do with p

    // or just delete it
    delete p;
}
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142