Pimpl模式

  • Post author:
  • Post category:其他




写在前面

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实现,代价就是开发及维护成本的提高。



版权声明:本文为SNAKEpc12138原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。