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
2
分享

创作者周榜

更多
正在热议
更多
# 一张图晒出你司的标语 #
4237次浏览 75人参与
# AI面会问哪些问题? #
27346次浏览 548人参与
# 开放七大实习专项,百度暑期实习值得冲吗 #
14977次浏览 220人参与
# 你的实习产出是真实的还是包装的? #
19973次浏览 342人参与
# 找AI工作可以去哪些公司? #
8806次浏览 228人参与
# 春招至今,你的战绩如何? #
64123次浏览 575人参与
# 米连集团26产品管培生项目 #
13270次浏览 285人参与
# 从事AI岗需要掌握哪些技术栈? #
8694次浏览 296人参与
# 你做过最难的笔试是哪家公司 #
32770次浏览 226人参与
# 中国电信笔试 #
31627次浏览 285人参与
# 投递几十家公司,到现在0offer,大家都一样吗 #
340658次浏览 2173人参与
# 阿里笔试 #
178225次浏览 1311人参与
# 第一份工作一定要去大厂吗 #
14318次浏览 122人参与
# 金三银四,你的春招进行到哪个阶段了? #
22006次浏览 280人参与
# 沪漂/北漂你觉得哪个更苦? #
9702次浏览 193人参与
# HR最不可信的一句话是__ #
6124次浏览 113人参与
# 应届生第一份工资要多少合适 #
20660次浏览 86人参与
# AI时代,哪个岗位还有“活路” #
11369次浏览 339人参与
# 春招你拿到offer了吗 #
830972次浏览 9986人参与
# 长得好看会提高面试通过率吗? #
22430次浏览 254人参与
# 聊聊你的职场新体验 #
336397次浏览 1894人参与
# 学历对求职的影响 #
665009次浏览 4249人参与
牛客网
牛客网在线编程
牛客网题解
牛客企业服务