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 the compilation time. More information about the idiom itself can be found, for example, here , here and here . This article is about what a smart pointer to use when working with Pimpl and why it is needed.
Consider the various options for implementing Pimpl:
The easiest way, which many probably saw, 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:
Minuses:
Immediately it should be noted 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 the problems associated with Pimpl.
Usage example:
// widget.h class Widget { // ... struct Impl; std::auto_ptr<Impl> 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 lifetime. Using the RAII idiom, auto_ptr allows you to work with Pimpl safely with respect to exceptions, since when an exception occurs, its destructor is called, which frees the memory.
Despite the automatic release of memory, auto_ptr has a very dangerous property when working with Pimpl. When executing this code, surprisingly many will leak memory without any warnings:
// widget.h class Widget { public: Widget(); //... private: struct Impl; std::auto_ptr<Impl> d_; }; // widget.cpp struct Widget::Impl { /*...*/ }; Widget::Widget(): d_(new Impl) {}
This is due to the fact that auto_ptr will delete the incomplete class. More details on this issue can be found here . Since this problem does not only apply to auto_ptr, it is strongly recommended that you read and understand this issue. The brief solution to the problem in this situation is the explicit declaration and definition of the destructor.
Pros:
Minuses:
In C ++ 11, move semantic appeared, which allowed auto_ptr to be replaced by a smart pointer with the expected unique_ptr behavior.
Usage example:
// widget.h class Widget { // ... struct Impl; std::unique_ptr<Impl> d_; }; // widget.cpp struct Widget::Impl { /*...*/ }; Widget::Widget(): d_(std::make_unique<Impl>()) {} Widget::~Widget() {}
unique_ptr solves the problem of deleting an incomplete class when checking for completeness at the compilation stage. Now silently delete the incomplete class will not work.
However, to solve the problem, unique_ptr still has the disadvantage 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<Impl> d_; }; // widget.cpp struct Widget::Impl { int i = 0; }; Widget::Widget(): d_(std::make_unique<Impl>()) {} Widget::~Widget() {} void Widget::foo() const { d_->i = 42; // <- }
In most cases, the compilation of such code is undesirable.
Although the pointer is used in the idiom Pimpl, 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 must be constant.
Pros:
Minuses:
In the experimental library, there is a wrapper for propagate_const pointers, which allows you to fix logical constancy.
Usage example:
// widget.h class Widget { // ... struct Impl; std::experimental::propagate_const<std::unique_ptr<Impl>> d_; }; // widget.cpp struct Widget::Impl { /*...*/ }; Widget::Widget(): d_(std::make_unique<Impl>()) {} Widget::~Widget() {}
Now the code from the previous example will cause compilation errors.
It 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 manifest itself at the first call to the class at runtime.
Pros:
Minuses:
Considering all the above minuses and advantages, for a complete solution it is necessary to provide a smart pointer that meets the following requirements:
The first two points can be implemented using unique_ptr:
template<class T> class PimplPtr { public: using ElementType = typename std::unique_ptr<T>::element_type; // ... private: std::unique_ptr<T> p_; // <- };
The third point could be implemented using propagate_const, but, since it is not yet in the standard, you can easily implement the methods of accessing the pointer 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 execute the fourth item, you need to implement a default constructor that will create an Impl:
PimplPtr(): p_(std::make_unique<T>()) {}
If Impl does not have a default constructor, the compiler will say this, and the user will need another constructor:
explicit PimplPtr(std::unique_ptr<T>&& p) noexcept: p_(std::move(p)) {}
For greater clarity, it may be worth adding static checks in the constructor and destructor:
PimplPtr(): p_(std::make_unique<T>()) { 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 the move, you must add the appropriate constructor and operator:
PimplPtr(PimplPtr&&) noexcept = default; PimplPtr& operator =(PimplPtr&&) noexcept = default;
All code in its entirety:
namespace utils { template<class T> class PimplPtr { public: using ElementType = typename std::unique_ptr<T>::element_type; PimplPtr(): p_(std::make_unique<T>()) { static_assert(sizeof(T) > 0, "Probably, you forgot to declare constructor explicitly"); } explicit PimplPtr(std::unique_ptr<T>&& 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<T> p_; }; } // namespace utils
Usage example:
// widget.h class Widget { // ... struct Impl; utils::PimplPtr<Impl> d_; }; // widget.cpp struct Widget::Impl { /*...*/ }; Widget::Widget() {} Widget::~Widget() {}
Using the developed pointer helps to avoid some silly mistakes and focus on writing useful code.
Source: https://habr.com/ru/post/311038/
All Articles