使用shared_ptr处理不完全类型的Pimpl习语



我正在阅读Scott Meyers的Effective Modern c++,他正在讨论使用pimpl习惯用法并指出unique_ptr的实现类,但是有一个特殊成员函数(例如析构函数)要求类型是完整的问题。这是因为在使用delete p之前,unique_ptr的默认删除器静态地判断要删除的类型是否完整。因此,类的任何特殊成员函数必须在实现文件中定义(而不是由编译器生成),之后定义实现类。

在本章的末尾,他提到如果使用的智能指针是shared_ptr,则不需要在实现文件中定义特殊的成员函数,这源于它支持自定义删除器的方式。引用:

std::unique_ptr和std::shared_ptr的行为差异pImpl指针源于这些智能指针支持自定义的不同方式删除人。对于std::unique_ptr,删除对象的类型是智能对象类型的一部分指针,这使得编译器可以生成更小的运行时数据结构和更快的运行时代码。效率提高的一个后果是当编译器生成特殊函数(例如:使用析构函数(move操作)。对于std::shared_ptr,对象的类型Deleter不是智能指针类型的一部分。这就需要更大的运行时数据结构和稍慢的代码,但指向类型不需要完整当使用编译器生成的特殊函数时。

尽管如此,我仍然看不出为什么shared_ptr仍然可以工作没有类是完整的。似乎在使用shared_ptr时没有编译器错误的唯一原因是因为没有像unique_ptr那样的静态断言,并且由于缺乏断言,可能会发生未定义的运行时行为。

我不知道shared_ptr的析构函数是如何实现的,但是(通过阅读c++ Primer)我得到了这样的印象:

del ? del(p) : delete p;

其中del是指向自定义删除器的指针或函数对象。Cppreference还明确了没有自定义删除器的shared_ptr析构函数使用delete p

3)如果T不是数组类型,则使用delete表达式delete ptr;…Y必须是完整类型。delete表达式必须格式良好,具有良好定义的行为,并且不抛出任何异常。

强调被删除的类型必须是完整的。粉刺习语的一个最小示例:

//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;
}

当编译main.cpp中的Widget a时,shared_ptr的模板被实例化为Widget类型(在main.cpp中),并且shared_ptr编译的析构函数可能包含delete pImpl行的执行,因为我没有提供自定义删除器。但是,此时Impl仍然没有定义,但是执行了delete pImpl行。这当然是未定义的行为?

那么,当使用shared_ptr的pimpl习语时,我如何不必在实现文件中定义特殊的成员函数来避免未定义的行为?

创建共享指针的删除器:

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

在此之前,所有共享指针都相当于std::funciton<void(Impl*)>

当你用T*构造shared_ptr时,它会写一个delete并将其存储在等效的std::function中。此时,类型必须是完整的。

因此,在完全定义Impl之后,您必须定义的唯一函数是那些从某种T*创建pImpl的函数。

最新更新