C++面试题:实现Pimpl模式时特殊成员函数需定义在实现里
定义:Pimpl模式是指pointer to implementation。也就是说将类Widget的实现完全放到另外一个类Impl里,而类Widget对外提供接口,这些接口的调用最终会通过Impl指针成员(裸指针或智能指针),调用相应的实现接口。
优势:这么做可以减少项目的依赖,进而减少不必要的编译。
一、使用裸指针实现Pimpl模式
// widget.h
class Widget {
public:
Widget();
~Widget();
private:
struct Impl;
Impl *pImpl;
};
// widget.cpp
#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(new Impl) {}
Widget::~Widget() {
delete pImpl;
}
假设widget.h会被100个cpp文件调用,当gadget.h变化时,编译器只会编译一个widget.cpp文件。如果没有采用Pimpl模式模式,那么最终编译器会编译101次。由此可见,Pimpl模式能减少编译的次数,进而加快编译速度!
但是,正如你看到的,你需要在构造函数和析构函数里操作(new和delete)裸指针。有没有更好的办法来构建和销毁Impl对象呢?答案是使用智能指针,首先我们看看使用unique_ptr会遇到的问题。
二、使用std::unique_ptr实现Pimpl模式
将以上例子的Impl*简单替换成std::unique_ptr,如下所示:
// widget.h
class Widget {
public:
Widget();
private:
struct Impl;
std::unique_ptr<Impl> pImpl;
};
// widget.cpp
#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>()) {}
}
// client.cpp
#include "widget.h"
Widget w; // 这里会报错!!
为什么在client.cpp里的调用会报错?根本原因在于std::unique_ptr的析构函数里调用了static_assert,该函数会在编译器间判断Impl的定义是否可见,而在client.cpp里,编译器只看到了Impl的前置声明,而没有完整的定义(完整的定义在widget.cpp里),因此编译器会报这种错误:在非完整型别上实施了sizeof或者delete。之所以会调用std::unique_ptr的析构函数,是因为编译器会自动生成Widget w的析构调用。
为了解决这个问题,我们需要把Widget的析构函数放到一个可以看得见Impl定义的地方,也就是widget.cpp。然而,当你在widget.cpp中定义Widget的析构函数时,编译器不会自动生成Widget的move操作,因此,你还需要在widget.cpp中定义Widget的move操作,如下所示:
// widget.h
class Widget {
public:
Widget();
~Widget();
Widget(Widget&& rhs); // 仅声明
Widget operator=(Widget&& rhs); // 仅声明
private:
struct Impl;
std::unique_ptr<Impl> pImpl;
};
// widget.cpp
Widget::Widget()
:pImpl(std::make_unique<Impl>()) {}
}
Widget::~Widget() = default;
Widget(Widget&& rhs) = default; // 注意!!
Widget operator=(Widget&& rhs) = default;
这里需要注意的是,把move操作的定义放到widget.h里也会出现同样的编译错误,为什么?原因有2个:1)当编译器遇到Widget(Widget&& rhs) = default,它会自动生成异常处理逻辑,而这段逻辑里会调用Widget的析构函数;2)当编译器遇到Widget operator=(Widget&& rhs) = default,它会自动生成一段代码:调用std::unique_ptr的析构函数来销毁pImpl,然后才完成赋值。
最后,由于std::unique_ptr是一个移动拷贝类型,因此,当你使用std::unique_ptr实现Pimpl模式时,需要实现深拷贝的copy操作。
三、使用std::shared_ptr实现Pimpl模式
std::shared_ptr实现Pimpl模式就容易多了。因为shared_ptr无需看见Impl的定义,所以编译器为Widget生成的析构函数就足够了。因为不需要手动定义Widget析构函数,因此编译器也会自动为Widget生成对应的移动操作。实现的细节如下:
// widget.h
class Widget {
public:
Widget();
private:
struct Impl;
std::shared_ptr<Impl> pImpl;
};
// widget.cpp
Widget::Widget()
:pImpl(std::make_shared<Impl>()) {}
}
// client.cpp
Widget w1;
auto w2(std::move(w1)); // 会自动生成移动构造函数,所以这里是移动!!
为什么std::unique_ptr和std::shared_ptr在实现Pimpl会有如此大的区别?
因为,std::unique_ptr的delete操作是std::unique_ptr的一部分,而std::shared_ptr的delete操作不是std::shared_ptr类型的一部分。
聊完以上知识点,感觉自己的C++技能又提升了一点。希望以上剖析的思路,能有效地帮助你学习以下经典C++书籍。
#C++##C++面试题##面试#