写在前面
Pimpl(Pointer to implementation,又称作“编译防火墙”) 是一种减少代码依赖和编译时间的C++编程技巧,其基本思想是将一个外部可见类(visible class)的实现细节(一般是所有私有的非虚成员)放在一个单独的实现类(implementation class)中,而在可见类中通过一个私有指针来间接访问该实现类。
下面通过一个简单示例说明为什么使用Pimpl、如何使用Pimpl。
类普通实现
这里创建一个简单的Fruit类,实现如下:
//Fruit.h
#pragma once
#include <string>
class Fruit
{
public:
Fruit();
~Fruit();
void display();
void setPrice(double dbPrice);
double getPrice() const;
private:
std::string m_sName;
double m_dbPrice;
};
//Fruit.cpp
#include "Fruit.h"
#include <iostream>
Fruit::Fruit() : m_sName(""), m_dbPrice(0.0)
{
std::cout << "Fruit::Fruit\n";
}
Fruit::~Fruit()
{
std::cout << "Fruit::~Fruit\n";
}
void Fruit::display()
{
std::cout << "Name: " << m_sName << ", Price: " << m_dbPrice << std::endl;
}
void Fruit::setPrice(double dbPrice)
{
std::cout << "ruit::setPrice: " << dbPrice << std::endl;
m_dbPrice = dbPrice;
}
double Fruit::getPrice() const
{
std::cout << "Fruit::getPrice\n";
return m_dbPrice;
}
在其他文件(例main函数)中引用类:
//main.cpp
#include <iostream>
#include "Fruit.h"
int main()
{
Fruit fruit;
fruit.setPrice(5.88);
fruit.display();
}
上面是常见的类定义及使用方式,这里可以很明显的发现两个问题:
①头文件暴露了私有成员。当然对于内部开发这无关紧要,但对于一些对外的模块开发(如dll),外部使用人员有可能通过对外的头文件中的私有成员,推测内部实现,这显然不是公司所乐意见到的。
②接口和实现耦合,存在严重编译依赖性。例上面示例只实现的price成员的对外接口,若添加name成员的对外接口(setName、getName), 所有引用Fruit.h头文件的源文件(Fruit.cpp, main.cpp)都需要重新编译,在大型的项目中,这会花费很多编译时间。
因此,对于需要对外隐藏信息或想要减少编译依赖的需求,可以Pimpl模式实现类。
Pimpl实现
在上面Fruit类的基础上调整:
//Fruit.h
#pragma once
//事先声明
class FruitPrivate;
class Fruit
{
public:
Fruit();
~Fruit();
void display();
void setPrice(double dbPrice);
double getPrice() const;
//为避免后续对头文件进行修改,可事先预留所有成员的对外接口
void setName(const std::string& sName);
std::string getName() const;
private:
//成员放至私有类
//std::string m_sName;
//double m_dbPrice;
FruitPrivate* m_priFruit;
};
//Fruit.cpp
#include "Fruit.h"
#include <iostream>
#include <string>
/***********************************FruitPrivate*********************************************/
class FruitPrivate
{
public:
FruitPrivate();
~FruitPrivate();
void display();
void setPrice(double dbPrice);
double getPrice() const;
private:
std::string m_sName;
double m_dbPrice;
};
FruitPrivate::FruitPrivate() : m_sName(""), m_dbPrice(0.0)
{
std::cout << "FruitPrivate::FruitPrivate\n";
}
FruitPrivate::~FruitPrivate()
{
std::cout << "PruitPrivate::~FruitPrivate\n";
}
void FruitPrivate::display()
{
std::cout << "FruitPrivate::display--Name: " << m_sName << ", Price: " << m_dbPrice << std::endl;
}
void FruitPrivate::setPrice(double dbPrice)
{
std::cout << "FruitPrivate::setPrice--price: " << dbPrice << std::endl;
}
double FruitPrivate::getPrice() const
{
std::cout << "FruitPrivate::getPrice";
return m_dbPrice;
}
/***********************************end FruitPrivate*********************************************/
Fruit::Fruit() : m_priFruit(new FruitPrivate)//m_sName(""), m_dbPrice(0.0)
{
std::cout << "Fruit::Fruit\n";
}
Fruit::~Fruit()
{
std::cout << "Fruit::~Fruit\n";
if (m_priFruit != nullptr)
{
delete m_priFruit;
}
}
void Fruit::display()
{
//std::cout << "Name: " << m_sName << ", Price: " << m_dbPrice << std::endl;
m_priFruit->display();
}
void Fruit::setPrice(double dbPrice)
{
//std::cout << "ruit::setPrice: " << dbPrice << std::endl;
//m_dbPrice = dbPrice;
m_priFruit->setPrice(dbPrice);
}
double Fruit::getPrice() const
{
//std::cout << "Fruit::getPrice\n";
//return m_dbPrice;
return m_priFruit->getPrice();
}
//按需实现
void Fruit::setName(const std::string& sName)
{
}
std::string Fruit::getName() const
{
}
在其他文件(上例main函数)中的引用不变。
可以看到上面调整后的头文件中不再对外展示私有成员,取而代之的是私有类的指针,原本的私有成员存放到私有类中,以实现隐藏。
另外,再添加name成员的对外接口(头文件已预留),只需重新编译Fruit.cpp即可,极大程度地减少编译依赖。
优点
①信息隐藏。私有成员完全可以隐藏在共有接口之外,尤其对于闭源API的设计尤其的适合。同时,很多代码会应用平台依赖相关的宏控制,这些琐碎的东西也完全可以隐藏在实现类当中,给用户一个简洁明了的使用接口。
②加速编译。这通常是用pImpl手法的最重要的收益,称之为编译防火墙(compilation firewall),主要是阻断了类的接口和类的实现两者的编译依赖性。这样,类用户不需要额外include不必要的头文件,同时实现类的成员可以随意变更,而公有类的使用者不需要重新编译。
③二进制兼容性。通常对一个类的修改,会影响到类的大小、对象的表示和布局等信息,那么任何该类的用户都需要重新编译才行。而且即使更新的是外部不可访问的private部分,虽然从访问性来说此时只有类成员和友元能否访问类的私有部分,但是私有部分的修改也会影响到类使用者的行为,这也迫使类的使用者需要重新编译。
而对于使用pImpl手法,如果实现变更被限制在实现类中,那公有类只持有一个实现类的指针,所以实现做出重大变更的情况下,pImpl也能够保证良好的二进制兼容性,这是pImpl的精髓所在。
缺点
①在私有类中对公有类的访问需另外设计实现。相较于常规实现,这显然会加大开发人员的时间成本,不过在Qt中,有提供Q指针和D指针,以支持公有类和私有类的相互访问,而无需另外实现。
②Pimpl对拷贝操作比较敏感,要么禁止拷贝操作,要么就需要自定义拷贝操作。每个类都需要对自己的所有成员的拷贝、赋值等操作负责。在公有类中虽然只有一个私有类的指针成员,但其(私有类)内部有多少成员,在外人看来不得而知,因此共有类和私有类都需担负起成员的拷贝、赋值等操作的责任。
③编译器将不再能够捕获const方法中对成员变量的修改。因为私有成员变量已经从公有类脱离到了实现类当中了,公有类的const只能保护指针值本身是否改变,而不再能进一步保护其所指向的数据。例上面对外的get接口,虽然在公有类中限制为const(不能修改私有类成员指针指向),但在调用的私有类对于接口的内部也有变动成员的可能(上例中在私用类对外的get接口后有加const限制)。
注意事项
pImpl最需要关注的就是共有类的复制语义,因为实现类是以指针的方式作为共有类的一个成员,而默认C++生成的拷贝操作只会执行对象的浅拷贝,这显然违背了pImpl的原本意图,除非是真的想要底层共享一个实现对象。针对这个问题,解决方式有:
①禁止复制操作 :将所有的复制操作定义为private的,或者继承 boost::noncopyable,或者在新标准中将这些复制操作定义为delete;
②显式定义复制语义:创建新的实现类对象,执行深拷贝。要么不定义拷贝、移动操作符,要定义就需要将他们全部重新定义。
优化
使用指针指针管理私有类指针成员,拷贝、赋值操作限制。
//Fruit.h
#pragma once
#include <string>
#include <memory>
class FruitPrivate;
class Fruit
{
public:
Fruit();
~Fruit();
//拷贝、赋值操作处理
Fruit(const Fruit&) = delete; //私有成员为指针,禁止浅拷贝
Fruit& operator=(const Fruit&) = delete; //禁止赋值操作
//可实现移动拷贝
Fruit(Fruit&&) = default;
Fruit& operator=(Fruit&&) = default;
void display();
void setPrice(double dbPrice);
double getPrice() const;
//为避免后续对头文件进行修改,可事先预留所有成员的对外接口
void setName(const std::string& sName);
std::string getName() const;
private:
//成员放至私有类
//std::string m_sName;
//double m_dbPrice;
//FruitPrivate* m_priFruit;
//使用智能指针管理私有类指针
std::unique_ptr<FruitPrivate> m_priFruit;
};
//Fruit.cpp
#include "Fruit.h"
#include <iostream>
/***********************************FruitPrivate*********************************************/
class FruitPrivate
{
public:
FruitPrivate();
~FruitPrivate();
//拷贝、赋值操作和公有类保持一致
FruitPrivate(const FruitPrivate&) = delete;
FruitPrivate& operator=(const FruitPrivate&) = delete;
FruitPrivate(FruitPrivate&&) = default;
FruitPrivate& operator=(FruitPrivate&&) = default;
void display();
void setPrice(double dbPrice);
double getPrice() const;
private:
std::string m_sName;
double m_dbPrice;
};
FruitPrivate::FruitPrivate() : m_sName(""), m_dbPrice(0.0)
{
std::cout << "FruitPrivate::FruitPrivate\n";
}
FruitPrivate::~FruitPrivate()
{
std::cout << "PruitPrivate::~FruitPrivate\n";
}
void FruitPrivate::display()
{
std::cout << "FruitPrivate::display--Name: " << m_sName << ", Price: " << m_dbPrice << std::endl;
}
void FruitPrivate::setPrice(double dbPrice)
{
std::cout << "FruitPrivate::setPrice--price: " << dbPrice << std::endl;
}
double FruitPrivate::getPrice() const
{
std::cout << "FruitPrivate::getPrice";
return m_dbPrice;
}
/***********************************end FruitPrivate*********************************************/
Fruit::Fruit() : m_priFruit(std::make_unique<FruitPrivate>())//m_sName(""), m_dbPrice(0.0)
{
std::cout << "Fruit::Fruit\n";
}
Fruit::~Fruit()
{
std::cout << "Fruit::~Fruit\n";
//if (m_priFruit != nullptr)
//{
// delete m_priFruit;
//}
}
void Fruit::display()
{
//std::cout << "Name: " << m_sName << ", Price: " << m_dbPrice << std::endl;
m_priFruit->display();
}
void Fruit::setPrice(double dbPrice)
{
//std::cout << "ruit::setPrice: " << dbPrice << std::endl;
//m_dbPrice = dbPrice;
m_priFruit->setPrice(dbPrice);
}
double Fruit::getPrice() const
{
//std::cout << "Fruit::getPrice\n";
//return m_dbPrice;
return m_priFruit->getPrice();
}
//按需实现
void Fruit::setName(const std::string& sName)
{
}
std::string Fruit::getName() const
{
return "";
}
总结
类的常规实现和Pimpl实现各有优劣。若只是为了快速开发且没有对外隐藏需求,常规实现无疑是很好的选择,若想要减少编译依赖且不想对外展示私有成员,可选择使用Pimpl实现,代价就是开发及维护成本的提高。