pimple idiom

我们知道,当对头文件进行更改时,包括它的所有源都需要重新编译。在大型项目和库中,由于即使对实现进行了很小的更改,每个人都必须等待一段时间才能编译代码,这可能会导致构建时间问题。

解决此问题的一种方法是使用PImpl Idiom,它将实现隐藏在hearer中,并包括一个可立即编译的接口文件。

The PImpl Idiom (Pointer to IMPLementation) 是一种用于将实现与接口分离的技术。这项技术通过把类中的成员变量替换成指向一个实现类(或结构体)的opaque pointer,成员变量被放进单独的实现类中,然后通过该指针间接获取原来的成员变量。

其最大程度地减少了hearer暴露,并帮助程序员减少了构建依赖性。

class Widget {      // 在头文件“widget.h”中
public:
    Widget();
    ...
private:
    std::string name;
    std::vector<double> data;
    Gadget g1, g2, g3;     // Gadget是某个用户定义的类型
};

因为Widget的成员变量有std::stringstd::vectorGadget,那么这些类型的头文件在Widget编译时必须出现,这意味Widget的用户必须要#include <string>,<vector>以及gadget.h

这些增加的头文件会增加Widget用户的编译时间,而且这使得用户依赖于这些头文件,即如果某个头文件的内容被改变了Widget的用户就要重新编译。 标准库头文件和不会经常改变,但是“gadget.h”可能会经常修改。

在C++98中使用 Pimpl Idiom,让Widget的成员变量替换成一个指向结构体的原生指针,这个结构体只被声明,没有被实现

class Widget {     // 依然在头文件“widget.h”中
public:
    Widget();
    ~Widget();
    ...
private:
    struct Impl;    // 声明实现类
    Impl *pImpl;    // 声明指针指向实现类
};

因为Widget不再提起std::stringstd::vectorGadget类型,所以Widget的用户不再需要“#include”那些头文件了。那样加快了编译速度,也意味着当头文件内容改变时,Widget的用户不会受到影响。

一个被声明,却没定义的类型称为不完整类型(incomplete type)。 Widget::Impl就是这样的类型,不完整类型能做的事情很少,不过可以声明一个指针指向它们,Pimpl Idiom就是利用了这个特性。

Pimpl Idiom的第一部分是声明一个指向不完整类型的指针作为成员变量,第二部分是动态分配和回收一个装有原来成员变量的对象,分配和回收的代码要写在实现文件,例如,对于Widget,写在“Widget.cpp”中:

#include "widget.h"      // 在实现文件“widget.cpp”
#include "gadget.h"
#include <string>
#include <vector>
                              `
struct Widget::Impl {   // 用原来对象的成员变量来定义实现类
    std::string name;
    std::vector<double> data;
    Gadget g1, g2, g3;
};
                            `
Widget::Widget() : pImpl(new Impl) {}  // 为Widget对象动态分配成员变量                          
Widget::~Widget() { delete pImpl; }  // 销毁这个对象的成员变量

依赖已经从“widget.h”(它被所有Widget类的使用者包含,并且对他们可见)转移到“widget.cpp”(该文件只被Widget类的实现者包含,并只对它可见)。现在,即使gadget.h发生了任何改变,影响的也仅仅是“widget.cpp”而与“widget.h”无关,而“widget.h” 并不需要include “widget.cpp” 。 不过这个代码是动态分配的,需要在Widget的析构函数中回收分配的对象。

实例

在这里插入图片描述
实现方法:

  1. 创建一个单独的class(或struct)以实现
  2. 将所有header的私有成员放到这个类中
  3. 在头文件中定义一个实现类(Impl)
  4. 在头文件中,创建一个指向实现类的前向声明(指针)
  5. 定义一个析构函数和一个复制/赋值运算符

明确声明析构函数的原因是,在编译时,智能指针(std :: unique_ptr)检查类型定义中是否存在可见的析构函数,如果仅前向声明,则会引发编译错误。

Example:

  • 头文件中包含的类定义是该类的公共接口
  • 我们定义unique pointer(std::unique_ptr)而不是原始的指针,因为接口类型的对象负责对象的生存期
  • 由于std :: unique_ptr是完整类型,因此需要用户声明的析构函数和复制/赋值运算符才能使实现类完整
  • 从用户的角度来看,The pimpl approach是透明的。在内部,对IMPLementation结构所做的更改仅影响包含它的文件(User.cpp)。这意味着用户无需重新编译即可应用这些更改。
/* |INTERFACE| User.h file */
  
#pragma once 
#include <memory> // PImpl 
#include <string> 
using namespace std; 
  
class User { 
public: 
    // Constructor and Destructors 
  
    ~User(); 
    User(string name); 
  
    // Asssignment Operator and Copy Constructor 
  
    User(const User& other); 
    User& operator=(User rhs); 
  
    // Getter 
    int getSalary(); 
  
    // Setter 
    void setSalary(int); 
  
private: 
    // Internal implementation class 
    class Impl; 
  
    // Pointer to the internal implementation 
    unique_ptr<Impl> pimpl; 
}; 
/* |IMPLEMENTATION| User.cpp file */

#include "User.h" 
#include <iostream> 
using namespace std; 

struct User::Impl { 

	Impl(string name) 
		: name(name){}; 

	~Impl(); 

	void welcomeMessage() 
	{ 
		cout << "Welcome, "
			<< name << endl; 
	} 

	string name; 
	int salary = -1; 
}; 

// Constructor connected with our Impl structure 
User::User(string name) 
	: pimpl(new Impl(name)) 
{ 
	pimpl->welcomeMessage(); 
} 

// Default Constructor 
User::~User() = default; 

// Assignment operator and Copy constructor 

User::User(const User& other) 
	: pimpl(new Impl(*other.pimpl)) 
{ 
} 

User& User::operator=(User rhs) 
{ 
	swap(pimpl, rhs.pimpl); 
	return *this; 
} 

// Getter and setter 
int User::getSalary() 
{ 
	return pimpl->salary; 
} 

void User::setSalary(int salary) 
{ 
	pimpl->salary = salary; 
	cout << "Salary set to "
		<< salary << endl; 
} 

PImpl的优点:

  • 二进制兼容性:二进制接口独立于私有字段。对实现进行更改不会使相关代码停滞不前。
  • 编译时间:由于只需要重建实现文件而不是每个客户端都重新编译其文件,因此编译时间减少了。
  • 数据隐藏:可以轻松隐藏某些内部细节,例如实现技术和用于实现公共接口的其他库。

PImpl的缺点:

  • 内存管理:由于分配的内存多于默认结构,因此内存使用量可能会增加,这对于嵌入式软件开发至关重要。
  • 维护工作:由于为了使用pimpl和附加的指针间接调用而增加了类,因此维护变得更加复杂(接口只能通过指针/引用使用)。
  • 继承:隐藏的实现(Hidden implementation)无法继承,尽管PImpl类可以

More example:

#include <iostream>
#include <memory>
#include <experimental/propagate_const>
 
// interface (widget.h)
class widget {
    class impl;
    std::experimental::propagate_const<std::unique_ptr<impl>> pImpl;
 public:
    void draw() const; // public API that will be forwarded to the implementation
    void draw();
    bool shown() const { return true; } // public API that implementation has to call
    widget(int);
    ~widget(); // defined in the implementation file, where impl is a complete type
    widget(widget&&); // defined in the implementation file
                      // Note: calling draw() on moved-from object is UB
    widget(const widget&) = delete;
    widget& operator=(widget&&); // defined in the implementation file
    widget& operator=(const widget&) = delete;
};
 
// implementation (widget.cpp)
class widget::impl {
    int n; // private data
 public:
    void draw(const widget& w) const {
        if(w.shown()) // this call to public member function requires the back-reference 
            std::cout << "drawing a const widget " << n << '\n';
    }
    void draw(const widget& w) {
        if(w.shown())
            std::cout << "drawing a non-const widget " << n << '\n';
    }
    impl(int n) : n(n) {}
};
void widget::draw() const { pImpl->draw(*this); }
void widget::draw() { pImpl->draw(*this); }
widget::widget(int n) : pImpl{std::make_unique<impl>(n)} {}
widget::widget(widget&&) = default;
widget::~widget() = default;
widget& widget::operator=(widget&&) = default;
 
// user (main.cpp)
int main()
{
    widget w(7);
    const widget w2(8);
    w.draw();
    w2.draw();
}

Reference:

  • https://en.cppreference.com/w/cpp/language/pimpl
  • https://www.geeksforgeeks.org/pimpl-idiom-in-c-with-examples/
  • Effective Modern C++ 条款22
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值