我们知道Pimpl习惯用法(pointer to implementation,即指涉到实现的指针)就是把某类的数据成员用一个指涉到某实现类的指针替代,然后把原来在主类中的数据成员放置到实现类中,并通过指针间接访问这些数据成员,例如,考虑Widget类定义如下:
//Widget.h中
class Widget {
public:
Widget();
private:
std::string name;
std::vector<double> data;
Gadget g1,g2,g3;
};
因为Widget的数据成员有std::string、std::vector等多个类型,所以相应的头文件必须存在,Widget才能通过编译,这也就要求外部客户如果想要使用Widget类,则必须包含<string>、<vector>、“gadget.h”,这些头文件增加了用户侧的编译时间,而且如果头文件发生变更--“gadget.h”增加了一个新接口,则Widget客户必须重新编译才行。
C++98中的Pimpl习惯用法,就是用一个指涉到已声明但未定义的结构的裸指针来替代Widget的数据成员,此时Impl是一个已声明但未定义的型别,叫做非完整型别,针对非完整型别,可以做的事情很有限,但声明一个指涉到它的指针就是其中之一。示例代码如下:
// Widget.h
class Widget {
public:
Widget();
~Widget();
private:
struct Impl; // 声明未定义--非完整型别
Impl* pImpl;
};
// Widget.cpp
#include <string>
#include <vector>
#include "gadget.h"
#include "Widget.h"
struct Widget::Impl {
std::string name;
std::vector<double> dta;
GGadget g1,g2,g3;
};
Widget::Widget():pImpl:(new Impl){}
Widget::~Widget(){
delete Widget;
}
以上是C++98的代码,对于C++11,我们应该首选智能指针而非裸指针,C++11示例代码如下:
//Widget.h中
class Widget {
public:
Widget();
private:
class Impl; // 声明未定义--非完整型别
std::unique_ptr<Impl> pImpl; // 智能指针
};
// Widget.cpp
#include <string>
#include <vector>
#include "gadget.h"
#include "Widget.h"
struct Widget::Impl {
std::string name;
std::vector<double> dta;
GGadget g1,g2,g3;
};
Widget::Widget():pImpl(std::make_unique<Impl>()){
}
//main.cpp
#include "Widget.h"
Widget w; //调用Widget实例,但此时会编译报错
以上代码中在main.cpp中调用Widget实例会提示编译报错,原因是,Widget实例w在析构时,会用调用智能指针pImpl的默认析构函数,我们知道默认析构函数时inline型别的,所以相当于在Widget.h文件内调用Impl的析构函数,而在Widget.h文件内,Impl是非完整型别的(只有声明,没有定义,其定义是在Impl.h内),从而提示编译报错。
为了解决上述问题,我们只需要自定义实现Widget的析构函数即可,并将其放在Widget.cpp中Impl的定义之后即可。
另外我们还应该知道的是,若默认析构函数的作用仅仅只是为了阻止编译期在头文件内自动生成析构函数,我们可以在Widget.cpp内将函数实现使用“=default”代替即可,例如:
Widget::~Widget() = default;
《Effective Modern C++》学习笔记之条款十七:理解特种成员函数的生成机制中我们说过,一个类中,如果自定义了析构函数,编译器将被阻止生成默认移动操作,所以如果需要类支持移动操作,就必须人为实现,所以,仿照上述例子,我们将其定义cpp文件中,代码如下:
// Widget.cpp
#include <string>
#include <vector>
#include "gadget.h"
#include "Widget.h"
struct Widget::Impl {
std::string name;
std::vector<double> dta;
GGadget g1,g2,g3;
};
Widget::Widget():pImpl(std::make_unique<Impl>()){
}
Widget::~Widget() = default;
Widget::Widget(Widget&& rhs) = default;
Widget& Widget::operator=(Widget&& rhs) = default;
另外,在本例中,我们使用的是std::unique_ptr(独占式智能指针),它是不支持复制操作的,所以,如果需要Widget支持复制操作,则我们必须自定义复制操作,示例代码如下:
//Widget.h中
class Widget {
public:
Widget();
Widget(Widget&& rhs);
Widget& operator=(Widget&& rhs);
Widget::~Widget();
Widget(Widget& rhs);
Widget& operator=(constWidget& rhs);
private:
class Impl; // 声明未定义--非完整型别
std::unique_ptr<Impl> pImpl; // 智能指针
};
// Widget.cpp
#include <string>
#include <vector>
#include "gadget.h"
#include "Widget.h"
struct Widget::Impl {
std::string name;
std::vector<double> dta;
GGadget g1,g2,g3;
};
Widget::Widget():pImpl(std::make_unique<Impl>()){
}
Widget::~Widget() = default;
Widget::Widget(Widget&& rhs) = default;
Widget& Widget::operator=(Widget&& rhs) = default;
Widget::Widget(Widget& rhs)pImpl(std::make_unique<Impl>(*rhs.pImpl)){
}
Widget::Widget& operator=(constWidget& rhs) {
*pImpl = *rhs.pImpl;
return *this;
}
在本例中,我们选择是unique_ptr,主要是可以更好的介绍本条款内容,而如果我们选择的是shared_ptr,那么无需在Widget中声明析构函数,又因为没有了析构函数,默认的移动操作也是满足要求的:
//Widget.h中
class Widget {
public:
Widget();
private:
class Impl; // 声明未定义--非完整型别
std::shared_ptr<Impl> pImpl; // 智能指针
};
此时一切编译和运行结果都符合预期,w会被默认构造,其值会被移动到w1中,然后又从w1中移动回来 ,最后,两个都析构掉。
//main.cpp
#include "Widget.h"
Widget w;
auto w1(std::move(w));
w = std::move(w1);
而std::shared_ptr和std::unique_ptr的在本例中区别如此之大的一个最大原因就是:它们对于自定义析构器的支持不同,std::unique_ptr中,自定义析构器是智能指针的一部分,其要求智能指针指涉到的对象必须是一个完整类型。
但对于std::shared_ptr而言,自定义析构器并非其型别的一部分,所以并不会要求其指涉到的对象必须是一个完整类型。