C++面试题:实现Pimpl模式时特殊成员函数需定义在实现里

alt

定义: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++书籍。

alt

#C++##C++面试题##面试#
全部评论

相关推荐

评论
2
3
分享
牛客网
牛客企业服务