Smart pointer for Pimpl


Pimpl (pointer to implementation, pointer to implementation) is a useful idiom common in C ++. This idiom has several positive aspects, however, in this article it is considered only as a means of reducing the dependencies of compilation time. More details about the idiom itself can be seen, for example, here , here and here . This article focuses on which smart pointer to use when working with Pimpl and why it is needed.


Consider the various options for implementing Pimpl:


Bare pointer


The easiest way that many have probably seen is to use a bare pointer.


Usage example:


// widget.h
class Widget {
public:
    Widget();
    ~Widget();
//...
private:
    struct Impl;
    Impl* d_;
};
// widget.cpp
struct Widget::Impl { /*...*/ };
Widget::Widget(): d_(new Impl) {}
Widget::~Widget() { delete d_; }

Pros:


  • no additional entities needed

Minuses:


  • the need to explicitly delete the pointer (a possible memory leak about which no one will tell)
  • it is not safe with regard to exceptions (if an exception occurs in the constructor after creating Impl, a memory leak will occur) - in general, this is the main reason why you need to use a smart pointer.

Using std :: auto_ptr


It should be noted right away that auto_ptr is already prohibited and should not be used. However, it is important to note its advantages over the bare pointer, as well as problems associated with Pimpl.


Usage example:


// widget.h
class Widget {
// ... как раньше
    struct Impl;
    std::auto_ptr d_;
};
// widget.cpp
struct Widget::Impl { /*...*/ };
Widget::Widget(): d_(new Impl) {}
Widget::~Widget() {}

auto_ptr, like other smart pointers from the standard library, takes responsibility for managing the pointer's lifetime. Using the RAII idiom, auto_ptr allows you to work with Pimpl safely with respect to exceptions, because when an exception occurs, its destructor is called, which frees memory.


Despite the automatic release of memory, auto_ptr has a very dangerous property when working with Pimpl. When this code is executed, surprisingly many, a memory leak will occur without any warnings:


// widget.h
class Widget {
public:
    Widget();
//... отсутствует деструктор
private:
    struct Impl;
    std::auto_ptr d_;
};
// widget.cpp
struct Widget::Impl { /*...*/ };
Widget::Widget(): d_(new Impl) {}

This is because auto_ptr will delete an incomplete class. More details about this problem can be found here . Since this problem applies not only to auto_ptr, it is strongly recommended that you familiarize yourself with this issue. A brief solution to the problem in this situation is to explicitly declare and define the destructor.


Pros:


  • safe for exceptions

Minuses:


  • forbidden
  • possible memory leak when deleting an incomplete class


Using std :: unique_ptr


In C ++ 11, move semantic appeared, which allowed replacing auto_ptr with a smart pointer with the expected unique_ptr behavior.


Usage example:


// widget.h
class Widget {
// ... как раньше
    struct Impl;
    std::unique_ptr d_;
};
// widget.cpp
struct Widget::Impl { /*...*/ };
Widget::Widget(): d_(std::make_unique()) {}
Widget::~Widget() {}

unique_ptr solves the problem of deleting an incomplete class when checking for completeness at the compilation stage. Now silently deleting an incomplete class will fail.


However, to solve the task, unique_ptr still has the drawback that it has the semantics of a regular pointer. Consider an example:


// widget.h
class Widget {
public:
// ... как раньше
    void foo() const; // <- константный метод
private:
    struct Impl;
    std::unique_ptr d_;
};
// widget.cpp
struct Widget::Impl { int i = 0; };
Widget::Widget(): d_(std::make_unique()) {}
Widget::~Widget() {}
void Widget::foo() const {
    d_->i = 42; // <- изменение данных внутри константного метода
}

In the vast majority of cases, compiling such code is undesirable.


Despite the fact that the Pimpl idiom uses a pointer, the data it points to have semantics of belonging to the original class. From the point of view of logical constancy, all data, including Impl data, in constant methods should be constant.


Pros:


  • memory leak protection

Minuses:


  • violation of logical constancy


Using std :: unique_ptr with propagate_const


There is a wrapper for propagate_const pointers in the experimental library that allows you to fix logical constancy.


Usage example:


// widget.h
class Widget {
// ... как раньше
    struct Impl;
    std::experimental::propagate_const> d_;
};
// widget.cpp
struct Widget::Impl { /*...*/ };
Widget::Widget(): d_(std::make_unique()) {}
Widget::~Widget() {}

Now the code from the previous example will cause compilation errors.


This seems to be close to a complete solution to the problem, however, there is another small point.
When writing a constructor, you must always explicitly create Impl. This does not seem to be a big problem, since most likely the error will appear when the class is first accessed at run time.


Pros:


  • observance of logical constancy

Minuses:


  • the ability to forget to create Impl in the constructor
  • propagate_const is not yet part of the standard


Using PimplPtr


Given all the pros and cons described above, for a complete solution, you must provide a smart pointer that meets the following requirements:


  • safety regarding exceptions
  • partial class deletion protection
  • observance of logical constancy
  • Impl created protection

The first two points can be implemented using unique_ptr:


template
class PimplPtr {
public:   
   using ElementType = typename std::unique_ptr::element_type;
 // ...
private:
   std::unique_ptr p_; // <- Должен быть неконстантный для семантики перемещения
};

The third item could be implemented using propagate_const, but since it is not yet in the standard, you can easily implement pointer access methods yourself:


    const ElementType* get() const noexcept { return p_.get(); }
    const ElementType* operator->() const noexcept { return get(); }
    const ElementType& operator*() const noexcept { return *get(); }
    explicit operator const ElementType*() const noexcept { return get(); }
    ElementType* get() noexcept { return p_.get(); }
    ElementType* operator->() noexcept { return get(); }
    ElementType& operator*() noexcept { return *get(); }
    explicit operator ElementType*() noexcept { return get(); }

To complete the fourth point, you need to implement the default constructor, which will create Impl:


   PimplPtr(): p_(std::make_unique()) {}

If Impl does not have a default constructor, then the compiler will say this, and the user will need another constructor:


    explicit PimplPtr(std::unique_ptr&& p) noexcept: p_(std::move(p)) {}

For clarity, it might be worth adding static checks in the constructor and destructor:


   PimplPtr(): p_(std::make_unique()) {
       static_assert(sizeof(T) > 0, "Probably, you forgot to declare constructor explicitly");
   }
   ~PimplPtr() {
       static_assert(sizeof(T) > 0, "Probably, you forgot to declare destructor explicitly");
   }

And, to preserve the semantics of movement, you need to add the appropriate constructor and operator:


   PimplPtr(PimplPtr&&) noexcept = default;
   PimplPtr& operator =(PimplPtr&&) noexcept = default;

Entire code:


namespace utils {
template
class PimplPtr {
public:   
   using ElementType = typename std::unique_ptr::element_type;
   PimplPtr(): p_(std::make_unique()) {
       static_assert(sizeof(T) > 0, "Probably, you forgot to declare constructor explicitly");
   }
   explicit PimplPtr(std::unique_ptr&& p): p_(std::move(p)) {}
   PimplPtr(PimplPtr&&) noexcept = default;
   PimplPtr& operator =(PimplPtr&&) noexcept = default;
   ~PimplPtr() {
       static_assert(sizeof(T) > 0, "Probably, you forgot to declare destructor explicitly");
   }
    const ElementType* get() const noexcept { return p_.get(); }
    const ElementType* operator->() const noexcept { return get(); }
    const ElementType& operator*() const noexcept { return *get(); }
    explicit operator const ElementType*() const noexcept { return get(); }
    ElementType* get() noexcept { return p_.get(); }
    ElementType* operator->() noexcept { return get(); }
    ElementType& operator*() noexcept { return *get(); }
    explicit operator ElementType*() noexcept { return get(); }
private:
   std::unique_ptr p_;
};
} // namespace utils

Usage example:


// widget.h
class Widget {
// ... как раньше
    struct Impl;
    utils::PimplPtr d_;
};
// widget.cpp
struct Widget::Impl { /*...*/ };
Widget::Widget() {}
Widget::~Widget() {}

Using a designed pointer helps to avoid some stupid mistakes and focus on writing useful code.


» Source Code


Also popular now: