For std::unique_ptr
pImpl pointers, declare special member functions in the class header, but implement them in the implementation file.
The Pimpl Idiom decreases build times by reducing compilation dependeencies between class clients and class implementation. For example:
1
2
3
4
5
6
7
8
9
|
// header file
class Widget {
public:
Widget();
...
private:
struct Impl;
std::unique_ptr<Impl> pImpl;
};
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// impl. file
#include "Widget.h"
#include "gadget.h"
#include <string>
#include <vector>
struct Widget::Impl {
std::string name;
std::vector<double> data;
Gadget g1, g2, g3;
};
Widget::Widget ()
: pImpl(std::make_unique<Impl>())
{}
|
However, even though the code above compiles, client code below won’t compile:
1
2
|
#include "Widget.h"
Widget w; // error!
|
The issue arises due to the code that’s generated when w
goes out of scope and gets destroyed:
- Since there’s no user defined destructor, destructor for
Widget
is generated by compiler, inside which there is a call to the destructor for pImpl
pImpl
is a std::unique_ptr<Widget::Impl>
using default deleter (a function that uses delete
on the raw pointer inside the std::unique_ptr
)
- prior to using
delete
, autogenerated implementation will have the default deleter employ C++11’s static_assert
to ensure the raw pointer doesn’t point to an incomplete type
- here this
static_assert
fails, because autogenerated destructors are implicitly inline
, and thus the definition of Widget::Impl
along with its autogenerated destructor inside Widget.cpp
hasn’t been seen by compilers
To solve the problem, we need to let the compiler see the body of Widget
’s destructor only inside the implementation file after Widget::Impl
has been defined:
1
2
3
4
5
6
7
8
9
10
|
// header file
class Widget {
public:
Widget();
~Widget(); // declaration only
...
private:
struct Impl;
std::unique_ptr<Impl> pImpl;
};
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// impl. file
#include "Widget.h"
#include "gadget.h"
#include <string>
#include <vector>
struct Widget::Impl {
std::string name;
std::vector<double> data;
Gadget g1, g2, g3;
};
Widget::Widget ()
: pImpl(std::make_unique<Impl>())
{}
Widget::~Widget()
{}
|
To emphasize the fact that the compiler-generated destructor would do the right thing, we can also write like this:
1
|
Widget::~Widget() = default;
|
The same reasoning goes with move operation:
- the compiler generated move assignment operator requires
Impl
to be complete because the object pointed to by pImpl
needs to be destroyed before assignment
- the compiler generated move constructor requires
Impl
to be complete because compilers must be able to generate code to destroy pImpl
in the event that an exception arises inside the move constructor (even it the constructor is noexcept
)
Once we added the move-related functions, compilers won’t generate copy operations for us. So to support a well-defined deep copy, we need to write our own version.
1
2
3
4
5
6
7
8
9
10
11
12
|
class Widget {
public:
Widget();
~Widget();
Widget(Widget&& rhs) noexcept;
Widget& operator=(Widget&& rhs) noexcept;
Widget(const Widget& rhs);
Widgt& operator=(const Widget& rhs);
private:
struct Impl;
std::unique_str<Impl> pImpl;
};
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
...
Widget::~Widget() = default;
Widget::Widget(Widget&& rhs) noexcept = default;
Widget& Widget::operator=(Widget&& rhs) noexcept = default;
Widget::Widget(const Widget& rhs)
: pImpl(nullptr)
{ if (rhs.pImpl) pImpl = std::make_unique<Impl>(*rhs.pImpl); }
Widget& Widget::operator=(const Widget& rhs)
{
if (!rhs.pImpl) pImpl.reset();
else if (!pImpl) pImpl = std::make_unique<Impl>(*rhs.pImpl);
else *pImpl = *rhs.pImpl;
return *this;
}
|
On the other hand, the above advice does not apply to std::shared_ptr
, because in std::shared_ptr
, the type of the deleter is not part of the type of the smart pointer, and pointed-to types need not be complete when compiler-generated special functions are employed:
1
2
3
4
5
6
7
8
|
class Widget {
public:
Widget();
... // no declaration for dtor or move operations
private:
struct Impl;
std::shared_ptr<Impl> pImpl;
};
|
1
2
3
4
|
// client code compiles without problem
Widget w1;
auto w2(std::move(w1)); // move-construct w2
w1 = std::move(w2); // move-assign w1
|