General topics | ||||||||||||||||
Flow control | ||||||||||||||||
Conditional execution statements | ||||||||||||||||
Iteration statements (loops) | ||||||||||||||||
Jump statements | ||||||||||||||||
Functions | ||||||||||||||||
Function declaration | ||||||||||||||||
Lambda function expression | ||||||||||||||||
inline specifier | ||||||||||||||||
Dynamic exception specifications(until C++17*) | ||||||||||||||||
noexcept specifier(C++11) | ||||||||||||||||
Exceptions | ||||||||||||||||
Namespaces | ||||||||||||||||
Types | ||||||||||||||||
Specifiers | ||||||||||||||||
| ||||||||||||||||
Storage duration specifiers | ||||||||||||||||
Initialization | ||||||||||||||||
Expressions | ||||||||||||||||
Alternative representations | ||||||||||||||||
Literals | ||||||||||||||||
Boolean -Integer -Floating-point | ||||||||||||||||
Character -String -nullptr(C++11) | ||||||||||||||||
User-defined(C++11) | ||||||||||||||||
Utilities | ||||||||||||||||
Attributes(C++11) | ||||||||||||||||
Types | ||||||||||||||||
typedef declaration | ||||||||||||||||
Type alias declaration(C++11) | ||||||||||||||||
Casts | ||||||||||||||||
Memory allocation | ||||||||||||||||
Classes | ||||||||||||||||
Class-specific function properties | ||||||||||||||||
| ||||||||||||||||
Special member functions | ||||||||||||||||
Templates | ||||||||||||||||
Miscellaneous | ||||||||||||||||
"Pointer to implementation" or "pImpl" is a C++programming technique that removes implementation details of a class from its object representation by placing them in a separate class, accessed through an opaque pointer:
// --------------------// interface (widget.h)struct widget{// public membersprivate:struct impl;// forward declaration of the implementation class// One implementation example: see below for other design options and trade-offsstd::experimental::propagate_const<// const-forwarding pointer wrapperstd::unique_ptr<// unique-ownership opaque pointer impl>> pImpl;// to the forward-declared implementation class}; // ---------------------------// implementation (widget.cpp)struct widget::impl{// implementation details};
This technique is used to construct C++ library interfaces with stable ABI and to reduce compile-time dependencies.
Contents |
Because private data members of a class participate in its object representation, affecting size and layout, and because private member functions of a class participate inoverload resolution (which takes place before member access checking), any change to those implementation details requires recompilation of all users of the class.
pImpl removes this compilation dependency; changes to the implementation do not cause recompilation. Consequently, if a library uses pImpl in its ABI, newer versions of the library may change the implementation while remaining ABI-compatible with older versions.
The alternatives to the pImpl idiom are
In simple cases, both pImpl and factory method remove compile-time dependency between the implementation and the users of the class interface. Factory method creates a hidden dependency on the vtable, and so reordering, adding, or removing virtual member functions breaks the ABI. The pImpl approach has no hidden dependencies, however if the implementation class is a class template specialization, the compilation firewall benefit is lost: the users of the interface must observe the entire template definition in order to instantiate the correct specialization. A common design approach in this case is to refactor the implementation in a way that avoids parametrization, this is another use case for the C++ Core Guidelines:
For example, the following class template does not use the typeT
in its private member or in the body ofpush_back
:
template<class T>class ptr_vector{std::vector<void*> vp;public:void push_back(T* p){ vp.push_back(p);}};
Therefore, private members can be transferred to implementation as-is, andpush_back
can forward to an implementation that does not useT
in the interface either:
// ---------------------// header (ptr_vector.hpp)#include <memory> class ptr_vector_base{struct impl;// does not depend on Tstd::unique_ptr<impl> pImpl;protected:void push_back_fwd(void*);void print()const;// ... see implementation section for special member functionspublic: ptr_vector_base(); ~ptr_vector_base();}; template<class T>class ptr_vector:private ptr_vector_base{public:void push_back(T* p){ push_back_fwd(p);}void print()const{ ptr_vector_base::print();}}; // -----------------------// source (ptr_vector.cpp)// #include "ptr_vector.hpp"#include <iostream>#include <vector> struct ptr_vector_base::impl{std::vector<void*> vp; void push_back(void* p){ vp.push_back(p);} void print()const{for(voidconst*const p: vp)std::cout<< p<<'\n';}}; void ptr_vector_base::push_back_fwd(void* p){ pImpl->push_back(p);}ptr_vector_base::ptr_vector_base(): pImpl{std::make_unique<impl>()}{}ptr_vector_base::~ptr_vector_base(){}void ptr_vector_base::print()const{ pImpl->print();} // ---------------// user (main.cpp)// #include "ptr_vector.hpp" int main(){int x{}, y{}, z{}; ptr_vector<int> v; v.push_back(&x); v.push_back(&y); v.push_back(&z); v.print();}
Possible output:
0x7ffd6200a42c0x7ffd6200a4300x7ffd6200a434
On the other hand, pImpl classes are move-friendly; refactoring a large class as movable pImpl may improve performance of algorithms that manipulate containers holding such objects, although movable pImpl has an additional source of runtime overhead: any public member function that is permitted on a moved-from object and needs access to private implementation incurs a null pointer check.
This section is incomplete Reason: Microbenchmark?) |
Use of pImpl requires a dedicated translation unit (a header-only library cannot use pImpl), introduces an additional class, a set of forwarding functions, and, if allocators are used, exposes the implementation detail of allocator use in the public interface.
Since virtual members are part of the interface component of pImpl, mocking a pImpl implies mocking the interface component alone. A testable pImpl is typically designed to allow full test coverage through the available interface.
As the object of the interface type controls the lifetime of the object of the implementation type, the pointer to implementation is usuallystd::unique_ptr.
Becausestd::unique_ptr requires that the pointed-to type is a complete type in any context where the deleter is instantiated, the special member functions must be user-declared and defined out-of-line, in the implementation file, where the implementation class is complete.
Because when const member function calls a function through a non-const member pointer, the non-const overload of the implementation function is called, the pointer has to be wrapped instd::experimental::propagate_const or equivalent.
All private data members and all private non-virtual member functions are placed in the implementation class. All public, protected, and virtual members remain in the interface class (seeGOTW #100 for the discussion of the alternatives).
If any of the private members needs to access a public or protected member, a reference or pointer to the interface may be passed to the private function as a parameter. Alternatively, the back-reference may be maintained as part of the implementation class.
If non-default allocators are intended to be supported for the allocation of the implementation object, any of the usual allocator awareness patterns may be utilized, including allocator template parameter defaulting tostd::allocator and constructor argument of typestd::pmr::memory_resource*.
This section is incomplete Reason: note connection to value-semantic polymorphism |
Demonstrates a pImpl with const propagation, with back-reference passed as a parameter, without allocator awareness, and move-enabled without runtime checks:
// ----------------------// interface (widget.hpp)#include <experimental/propagate_const>#include <iostream>#include <memory> class widget{class impl;std::experimental::propagate_const<std::unique_ptr<impl>> pImpl;public:void draw()const;// public API that will be forwarded to the implementationvoid draw();bool shown()const{returntrue;}// public API that implementation has to call widget();// even the default ctor needs to be defined in the implementation file// Note: calling draw() on default constructed object is UBexplicit widget(int); ~widget();// defined in the implementation file, where impl is a complete type widget(widget&&);// defined in the implementation file// Note: calling draw() on moved-from object is UB widget(const widget&)= delete; widget& operator=(widget&&);// defined in the implementation file widget& operator=(const widget&)= delete;}; // ---------------------------// implementation (widget.cpp)// #include "widget.hpp" class widget::impl{int n;// private datapublic:void draw(const widget& w)const{if(w.shown())// this call to public member function requires the back-referencestd::cout<<"drawing a const widget "<< n<<'\n';} void draw(const widget& w){if(w.shown())std::cout<<"drawing a non-const widget "<< n<<'\n';} impl(int n): n(n){}}; void widget::draw()const{ pImpl->draw(*this);}void widget::draw(){ pImpl->draw(*this);}widget::widget()=default;widget::widget(int n): pImpl{std::make_unique<impl>(n)}{}widget::widget(widget&&)=default;widget::~widget()=default;widget& widget::operator=(widget&&)=default; // ---------------// user (main.cpp)// #include "widget.hpp" int main(){ widget w(7);const widget w2(8); w.draw(); w2.draw();}
Output:
drawing a non-const widget 7drawing a const widget 8
This section is incomplete Reason: describe yet another alternative — "fast PImpl". The main difference is that the memory for the implementation is reserved in a data member that is an opaque C-array (inside the PImpl class definition), while in cpp file that memory is mapped (via reinterpret_cast or placement-new ) to the implementation structure. This approach has it's own pros and cons, in particular, an obviouspro is no extra allocation, on condition that enough memory was initially reserved atdesign-time of the PImpl class. (Whereas amongcons is reduced move-friendliness.) |
1. | GotW #28 : The Fast Pimpl Idiom. |
2. | GotW #100: Compilation Firewalls. |
3. | The Pimpl Pattern - what you should know. |