目录
一、Pimpl
二、Pimpl的实现
三、Pimpl的优缺点
1、Pimpl优点
2、Pimpl缺点
总结
Pimpl术语,即“pointer to implementation”(指向实现的指针),由Jeff Summer最先引入。该技巧可以避免在头文件中暴露私有细节,是促进API接口和实现保持完全分离的重要机制。
Pimpl并不是严格意义上的设计模式,而是桥接模式的一种特例。
它的目的是将文件间的编译依存关系降至最低,如果没有降低文件间的依存关系,这些头文件中有任何一个被改变,或者这些头文件所依赖的其他头文件有任何改变,那么任何使用该文件的文件也必须重新编译,这样的连串编译依存关系,会对许多项目造成难以形容的灾难
而我们可以将对象实现细节隐藏于一个指针背后,这样使编译依赖关系降到最低
Pimpl的本质是把一个类分割成两个类,一个类只提供接口,另一个类负责实现该接口
Pimpl的实现有两种,一种是将所有private成员放在一个class/struct中,这个类在头文件中仅做前置声明,在.cpp中定义。
#pragma once
#include
#include
#include
class Date;
class Address;
class Person
{
public:
Person(const std::string &name, const Date &birthday,
const std::string &addr);
std::string name() const;
std::string birthDate() const;
std::string Address() const;
private:
class PersonImpl;//前置声明
std::unique_ptr _pImpl;
};
这里使用PersonImpl来实现Person类内部的所有细节
同时使用_pImpl指针来辅助访问Person类内部,为了避免内存泄露,所以使用智能指针来管理那个资源
//Date类的简单实现
class Date
{
public:
Date(int year = 1970, int month = 1, int day = 1)
: _year(year), _month(month), _day(day)
{
}
std::string GetDate() const
{
return std::to_string(_year) + "/" + std::to_string(_month) + "/" + std::to_string(_day);
}
private:
int _year;
int _month;
int _day;
};
实现细节
struct Person::PersonImpl
{
std::string _name;
Date _birthday;
std::string _address;
};
Person::Person(const std::string &name, const Date &birthday,
const std::string &addr)
: _pImpl(new PersonImpl)
{
_pImpl->_name = name;
_pImpl->_birthday = birthday;
_pImpl->_address = addr;
}
std::string Person::name() const
{
return _pImpl->_name;
}
std::string Person::birthDate() const
{
return _pImpl->_birthday.GetDate();
}
std::string Person::Address() const
{
return _pImpl->_address;
}
#include "Person.hpp"
#include "comm.hpp"
int main()
{
Person p1("张三", Date(2002, 3, 5), "北京");
std::cout << p1.name() << std::endl;
std::cout << p1.birthDate() << std::endl;
std::cout << p1.Address() << std::endl;
return 0;
}
这种实现方式在上层使用与直接在Person类内部实现没有区别
另一种实现方式是令Person成为一种特殊的抽象类
class Date;
//interface class
class Person
{
public:
static std::shared_ptr Create(const std::string& name, const Date& birthday, const std::string& addr);
virtual ~Person();
virtual std::string name() const = 0;
virtual std::string birthDate() const = 0;
virtual std::string Address() const = 0;
};
类中除了Create函数声明为静态,其它所有的成员函数声明为纯虚方法,且类中没有成员变量
#pragma once
#include "Person.hpp"
class Date
{
public:
Date(int year = 1970, int month = 1, int day = 1)
: _year(year), _month(month), _day(day)
{
}
std::string GetDate() const
{
return std::to_string(_year) + "/" + std::to_string(_month) + "/" + std::to_string(_day);
}
private:
int _year;
int _month;
int _day;
};
class RealPerson : public Person
{
public:
RealPerson(const std::string &name, const Date &birthday, const std::string &addr)
: _theName(name), _theBirthdate(birthday), _theAdder(addr)
{
}
virtual ~RealPerson();
virtual std::string name() const;
virtual std::string birthDate() const;
virtual std::string Address() const;
private:
std::string _theName;
Date _theBirthdate;
std::string _theAdder;
};
std::shared_ptr Person::Create(const std::string& name, const Date& birthday, const std::string& addr)
{
return std::shared_ptr(new RealPerson(name, birthday, addr));
}
RealPerson::~RealPerson()
{
}
std::string RealPerson::name() const
{
return _theName;
}
std::string RealPerson::birthDate() const
{
return _theBirthdate.GetDate();
}
std::string RealPerson::Address() const
{
return _theAdder;
}
这里的Create函数的实现借助于它的派生类,因为派生类只继承了它的接口,并且Person类成员变量全部在RealPerson中,进一步屏蔽了Person类的实现细节
返回时返回的是它的派生类类型的智能指针,通过C++的切片,就可以安全的获得Person类
#include "Person.hpp"
#include "comm.hpp"
int main()
{
std::shared_ptr pp(Person::Create("张三", Date(2002, 3, 5), "北京"));
std::cout << pp->name() << std::endl;
std::cout << pp->birthDate() << std::endl;
std::cout << pp->Address() << std::endl;
return 0;
}
信息隐藏
实现细节可以隐藏到Impl类实现中,保护闭源API专有性。同时,接口头文件也能更干净、清晰表达真正的公有接口,易于阅读和理解。
降低耦合
接口类只用知道Impl类即可,不用包含私有成员变量所需头文件,也不必包含平台依赖的windows.h或sys/time.h。
加速编译
将实现相关头文件移入.cpp,API的引用层次降低,会导致编译时间减少。
更好的二进制兼容性
采用Pimpl的对象大小从不改变,因为对象总是单个指针大小。对私有成员变量做任何修改,都只影响隐藏在cpp文件内的实现类大小。而对象的二进制表示可以不变。
惰性分配
Impl类可以在需要时再构造,而不必在接口类构造时立即构造。
1)必须为你创建的每个对象分配并释放实现对象。这使得对象增加了一个指针(Impl* impl_),同时增加了通过指针访问成员的开销,增加了new和delete对象的开销。
2)必须通过impl_->的形式访问私有成员,给开发人员带来了不便。
3)编译器不能捕获接口类中const对成员变量修改。因为成员变量现在存在于独立的对象(impl_指针所指对象)中。编译器仅检查impl_指针是否发生变化,而不会检查其成员。
如果只是因为若干成本原因而放弃使用Pimpl是严重错误的,virtual也带来了若干成本,应该以渐进的方式使用这些技术,在工程中使用以求得代码有所变化时对服务带来最小冲击