【C++】Pimpl

目录

一、Pimpl

二、Pimpl的实现

三、Pimpl的优缺点

1、Pimpl优点

2、Pimpl缺点

总结


一、Pimpl

Pimpl术语,即“pointer to implementation”(指向实现的指针),由Jeff Summer最先引入。该技巧可以避免在头文件中暴露私有细节,是促进API接口和实现保持完全分离的重要机制。

Pimpl并不是严格意义上的设计模式,而是桥接模式的一种特例。

它的目的是将文件间的编译依存关系降至最低,如果没有降低文件间的依存关系,这些头文件中有任何一个被改变,或者这些头文件所依赖的其他头文件有任何改变,那么任何使用该文件的文件也必须重新编译,这样的连串编译依存关系,会对许多项目造成难以形容的灾难

而我们可以将对象实现细节隐藏于一个指针背后,这样使编译依赖关系降到最低

二、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;
}

三、Pimpl的优缺点

1、Pimpl优点

  • 信息隐藏
    实现细节可以隐藏到Impl类实现中,保护闭源API专有性。同时,接口头文件也能更干净、清晰表达真正的公有接口,易于阅读和理解。

  • 降低耦合
    接口类只用知道Impl类即可,不用包含私有成员变量所需头文件,也不必包含平台依赖的windows.h或sys/time.h。

  • 加速编译
    将实现相关头文件移入.cpp,API的引用层次降低,会导致编译时间减少。

  • 更好的二进制兼容性
    采用Pimpl的对象大小从不改变,因为对象总是单个指针大小。对私有成员变量做任何修改,都只影响隐藏在cpp文件内的实现类大小。而对象的二进制表示可以不变。

  • 惰性分配
    Impl类可以在需要时再构造,而不必在接口类构造时立即构造。

2、Pimpl缺点

1)必须为你创建的每个对象分配并释放实现对象。这使得对象增加了一个指针(Impl* impl_),同时增加了通过指针访问成员的开销,增加了new和delete对象的开销。

2)必须通过impl_->的形式访问私有成员,给开发人员带来了不便。

3)编译器不能捕获接口类中const对成员变量修改。因为成员变量现在存在于独立的对象(impl_指针所指对象)中。编译器仅检查impl_指针是否发生变化,而不会检查其成员。


总结

如果只是因为若干成本原因而放弃使用Pimpl是严重错误的,virtual也带来了若干成本,应该以渐进的方式使用这些技术,在工程中使用以求得代码有所变化时对服务带来最小冲击

你可能感兴趣的:(C++,c++)