Pimpl idiom using shared_ptr working with incomple

2020-06-18 10:13发布

问题:

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 if T 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?

回答1:

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.