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

程序员咋不秃头2024-05-13 14:10:17  91

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

概念

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

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

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

从一个例子入手

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

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

engine.h

class Engine { public: Engine = default;};

car_imp.h

#include "engine.h"class CarImp { public: CarImp = default; private: Engine engine_;};

car.h

#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

#include class CarImp;class Car { public: Car; void Start {} private: std::unique_ptr carimp_;};

car.cc

#include "car.h"#include "car_imp.h"Car::Car : carimp_(std:: make_unique ) {}

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

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

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

#include "car.h"int main { Car car; return 0;}

编译之后,报错如下:

car.cc:4:1: error: definition of explicitly-defaulted ‘Car::Car’ 4 | Car::Car : carimp_(std:: make_unique ) {} | ^~~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]’car.h:7:3: required from hereunique_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

#include class CarImp;class Car { public: Car; void Start {} ~Car; private: std::unique_ptr carimp_;};

car.cc

#include "car.h"#include "car_imp.h"Car::Car : carimp_(std:: make_unique ) {}Car::~Car = default;

转载此文是出于传递更多信息目的。若来源标注错误或侵犯了您的合法权益,请与本站联系,我们将及时更正、删除、谢谢。
https://www.414w.com/read/498096.html
0
最新回复(0)