SergeantPenguin SergeantPenguin - 11 months ago 79
C++ Question

Pimpl idiom using shared_ptr working with incomplete types

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

, but there is an issue of special member functions (such as destructors) requiring the type to be complete. This is because
'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
, 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
could still work without the class being complete. It seems like the only reason there is no compiler error when using
is because there is no static assertion like
had, and that undefined runtime behaviour could instead occur because of this lack of assertion.

I don't know the implementation of the
's destructor, but (from reading C++ Primer) I gathered the impression it works something like:

del ? del(p) : delete p;

is a pointer or function object to the custom deleter. Cppreference also makes it clear the
destructor with no custom deleter uses
delete p

3) Uses the delete-expression
delete ptr
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:


#ifndef WIDGET
#define WIDGET

#include <memory>

class Widget{
struct Impl;
std::shared_ptr<Impl> pImpl;


#endif // WIDGET


#include <string>
#include "Widget.h"

struct Widget::Impl{
std::string name;

Widget::Widget(): pImpl(new Impl) {}


#include <iostream>
#include "Widget.h"

int main(){
Widget a;

Widget a
is compiled, the template of
is instantited for type
) and presumably the resulting compiled destructor for
contains execution of the line
delete pImpl
, because I have not supplied a custom deletor. However at that point,
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
, I don't have to define the special member functions in the implementation file to avoid undefined behaviour?

Answer Source

The deleter for a shared pointer is created here:

Widget::Widget(): pImpl(new Impl) {}

until that point, all the shared pointer has is the equivalent of a std::funciton<void(Impl*)>.

When you construct a shared_ptr with a T*, it writes a deleter and stores it in the std::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 a pImpl from a T* of some kind.