I'm reading Effective Modern C++ by Scott Meyers and he's discussing the use of the pimpl idiom and pointing to the implementation class with unique_ptr
, but there is an issue of special member functions (such as destructors) requiring the type to be complete. This is because unique_ptr
's default deleter statically asserts whether the type to be deleted is complete, before delete p
is used. So any special member functions of the class must be defined in the implementation file (rather than being compiler-generated), after the implementation class has been defined.
At the end of the chapter, he mentions there is no need to define special member functions in the implementation file if the smart pointer used is shared_ptr
, and this stems from the way it supports a custom deleter. To quote:
The difference in behavior between std::unique_ptr and std::shared_ptr for pImpl pointers stems from the differing ways these smart pointers support custom deleters. For std::unique_ptr, the type of the deleter is part of the type of the smart pointer, and this makes it possible for compilers to generate smaller runtime data structures and faster runtime code. A consequence of this greater efficiency is that pointed-to types must be complete when compiler-generated special functions (e.g., destructors or move operations) are used. For std::shared_ptr, the type of the deleter is not part of the type of the smart pointer. This necessitates larger runtime data structures and somewhat slower code, but pointed-to types need not be complete when compiler-generated special functions are employed.
Despite this, I still can't see why shared_ptr
could still work without the class being complete. It seems like the only reason there is no compiler error when using shared_ptr
is because there is no static assertion like unique_ptr
had, and that undefined runtime behaviour could instead occur because of this lack of assertion.
I don't know the implementation of the shared_ptr
's destructor, but (from reading C++ Primer) I gathered the impression it works something like:
del ? del(p) : delete p;
Where del
is a pointer or function object to the custom deleter. Cppreference also makes it clear the shared_ptr
destructor with no custom deleter uses delete p
3) Uses the delete-expression
delete ptr
ifT
is not an array type; .... Y must be a complete type. The delete expression must be well formed, have well-defined behavior and not throw any exceptions.
Emphasis on the fact that the deleted type must be complete. A minimal example of the pimpl idiom:
//widget.h
#ifndef WIDGET
#define WIDGET
#include <memory>
class Widget{
public:
Widget();
private:
struct Impl;
std::shared_ptr<Impl> pImpl;
};
#endif // WIDGET
//widget.cpp
#include <string>
#include "Widget.h"
struct Widget::Impl{
std::string name;
};
Widget::Widget(): pImpl(new Impl) {}
//main.cpp
#include <iostream>
#include "Widget.h"
int main(){
Widget a;
}
When Widget a
in main.cpp
is compiled, the template of shared_ptr
is instantited for type Widget
(within main.cpp
) and presumably the resulting compiled destructor for shared_ptr
contains execution of the line delete pImpl
, because I have not supplied a custom deletor. However at that point, Impl
still has not been defined, yet the line delete pImpl
is executed. This, surely, is undefined behaviour?
So how is it that when using the pimpl idiom with shared_ptr
, I don't have to define the special member functions in the implementation file to avoid undefined behaviour?
The deleter for a shared pointer is created here:
until that point, all the shared pointer has is the equivalent of a
std::funciton<void(Impl*)>
.When you construct a
shared_ptr
with aT*
, it writes a deleter and stores it in thestd::function
equivalent. At that point the type must be complete.So the only functions you have to define after
Impl
is fully defined are those that create apImpl
from aT*
of some kind.