前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从示例入手了解惯用法之PIMPL

从示例入手了解惯用法之PIMPL

作者头像
高性能架构探索
发布2024-04-17 16:14:52
780
发布2024-04-17 16:14:52
举报
文章被收录于专栏:技术随笔心得技术随笔心得

你好,我是雨乐!

今天我们聊聊项目中一个常用的用法`PIMPL。

概念

PIMPLpointer to implementation的缩写,意指指向实现的指针,是一种广泛使用的减少编译依赖性的技术。

PIMPL主要目的是隐藏类的实现细节,对于减少编译时依赖性和打破头文件之间的循环依赖性特别有用,同时降低耦合度,提高ABI(Application Binary Interface)稳定性,以及简化跨编译单元的共享库升级。

相信很多人在开发的时候,为了解决编译不过的问题,在自己的头文件中增加了很多用不到的其它的头文件,而这样不仅违背了信息隐藏原则,编译时间也会显著增加。正是基于这个原因,才引入了PIMPL这一惯用法。

从一个例子入手

为了从直观上了解PIMPL带来的好处,我们且看一个例子。

在这个例子中,包含三个类,分别在car.h、engine.h以及car_imp.h中。

engine.h

代码语言:javascript
复制
class Engine {
 public:
  Engine() = default;
};

car_imp.h

代码语言:javascript
复制
#include "engine.h"

class CarImp {
 public:
  CarImp() = default;
 private:
  Engine engine_;
};

car.h

代码语言:javascript
复制
#include "car_imp.h"

class Car {
 public:
  Car() = default;
  
  void Start() {}
 private:
  CarImp carimp_;
};

从car.h中,可以看出,这里面存在一个依赖,即:如果要使用car这个类,不仅仅要包含其头文件,也需要知道car_imp.h。从设计的角度来看,car_imp.h应该被隐藏或者说不被使用car.h的用户看到,显然,上面这个设计不满足。

另一方面,正如我们所知道的,类的变量和函数都是在头文件中声明或定义的,如果头文件发生了更改,那么须重新编译包含相关头文件的所有其他模块。这将意味着大型项目会出现严重耗时的情况。

如果我们依赖了很多头文件,emm,耗时可想而知。。。

横空出世

正如前面代码中类Car所示,其所依赖的CarImp成员变量为其私有,对于对象类型的变量,必须包含其相应的头文件car_imp.h,否则将会编译失败,如果将其声明为指针方式呢?

且看看PIMPL的实现方式,代码如下:

car.h

代码语言:javascript
复制
#include <memory>

class CarImp;

class Car {
 public:
  Car();
  void Start() {}
 private:
  std::unique_ptr<CarImp> carimp_;
};

car.cc

代码语言:javascript
复制
#include "car.h"
#include "car_imp.h"

Car::Car() : carimp_(std:: make_unique <CarImp>()) {}

与上节的例子相比,carimp_仍然作为Car类的私有成员变量,与之前不同的是,这本例中其类型为std::unique_ptr,且增加了CarImp类的前置声明,表明该文件中未提供CarImp类的完整定义。

其次,本例中,头文件car.h和car_imp.h被移到了car.cc中。

好了,不妨使用如上代码:

代码语言:javascript
复制
#include "car.h"

int main() {
  Car car;

  return 0;
}

编译之后,报错如下:

代码语言:javascript
复制
car.cc:4:1: error: definition of explicitly-defaulted ‘Car::Car()’
    4 | Car::Car() : carimp_(std:: make_unique <CarImp>()) {}
      | ^~~
In file included from car.cc:1:
car.h:7:3: note: ‘constexpr Car::Car()’ explicitly defaulted here
    7 |   Car() = default;
      |   ^~~
In file included from /opt/rh/devtoolset-11/root/usr/include/c++/11/memory:76,
                 from car.h:1,
                 from main.cc:1:
unique_ptr.h: In instantiation of ‘void std::default_delete<_Tp>::operator()(_Tp*) const [with _Tp = CarImp]’:
unique_ptr.h:361:17:   required from ‘std::unique_ptr<_Tp, _Dp>::~unique_ptr() [with _Tp = CarImp; _Dp = std::default_delete<CarImp>]’
car.h:7:3:   required from here
unique_ptr.h:83:23: error: invalid application of ‘sizeof’ to incomplete type ‘CarImp’
   83 |         static_assert(sizeof(_Tp)>0,
      |                       ^~~~~~~~~~~

好了,现在开始着手解决上述报错~

析构函数可见性

在c++中,有一条这样的规则:如果指针的类型为void*或者指向的类型不完整(前向声明),则删除指针可能会导致未定义的行为。

在上面的例子中,在头文件car.h中,CarImp仅被前向声明,因此删除它的指针将导致未定义行为。

对于std::unique_ptr来说,在调用删除之前检查会类型的定义是否可见。如果仅向前声明该类型,则std::unique_ptr拒绝编译以及调用删除,从而防止潜在的未定义行为。

标准规定,如果定义的类中,为声明析构函数,则编译器会帮忙生成它,但是,编译器生成的方法被声明inline,因此直接在头文件中实现,又因为头文件中仅仅是前向声明,类型并不完整,这就导致类编译失败。

继续回到我们的例子,如果不为类Car编写析构函数,编译器会默认生成,为了不让编译器生成,则需要我们自己声明一个析构函数,又因为CarImp在头文件car.h中仅仅作为前向声明,所以这就要求我们将析构函数定义在.cc中,好了,直接看代码吧:

car.h

代码语言:javascript
复制
#include <memory>

class CarImp;

class Car {
 public:
  Car();
  void Start() {}
  ~Car();
 private:
  std::unique_ptr<CarImp> carimp_;
};

car.cc

代码语言:javascript
复制
#include "car.h"
#include "car_imp.h"

Car::Car() : carimp_(std:: make_unique <CarImp>()) {}
Car::~Car() = default;

好了,打完收工~

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2024-04-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 高性能架构探索 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 概念
  • 从一个例子入手
  • 横空出世
  • 析构函数可见性
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档