Pimpl模式

写在前面

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/54215.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

用MFC打开外部程序

在MFC&#xff08;Microsoft Foundation Classes&#xff09;中&#xff0c;你可以使用ShellExecute函数来打开Notepad并加载指定的文件。ShellExecute函数是Windows API的一部分&#xff0c;它可以执行与操作系统相关的操作&#xff0c;例如打开文件、运行程序等。 以下是在M…

算法修炼Day60|● 84.柱状图中最大的矩形

LeetCode:84.柱状图中最大的矩形 84. 柱状图中最大的矩形 - 力扣&#xff08;LeetCode&#xff09; 1.思路 双指针思路&#xff0c;以当前数组为中心&#xff0c;借助两个数组存放当前数柱左右两侧小于当前数柱高度的索引&#xff0c;进行h*w的计算。注意首尾节点的左侧索引…

牛客练习赛 114

C.Kevin的七彩旗 思路&#xff1a;贪心和dp均可以解决。 贪心&#xff1a;我们可以发现&#xff0c;最终想要获得合法的序列&#xff0c;我们必须是通过把几段连续的序列拼凑起来&#xff0c;但序列之间可能有重合&#xff0c;因此我们就转化为了&#xff0c;记录每一段最大的…

elementuiplus设置scroll-to-error之后 提示被遮挡的解决方案

项目场景&#xff1a; 普通的头部固定&#xff0c;中间滑动的布局&#xff0c;中间内容有表单&#xff0c;提交校验不通过时滚动到第一个错误项 问题描述 elementuiplus的scroll-to-error设置之后是局部滚动 当头部内容层级高于中间表单的时候&#xff0c;错误会被遮挡。 ---…

【HarmonyOS】实现将pcm音频文件进行编码并写入文件(API6 Java)

【关键字】 音频编码、管道模式、createEncoder 【写在前面】 在使用API6开发HarmonyOS应用时&#xff0c;如何将pcm源文件进行编码并写入文件&#xff0c;最后生成aac文件&#xff0c;本文直接附上主要代码开发步骤供大家参考。 【主要功能代码】 import ohos.media.codec.…

【C++】4、Preprocessor 预处理:条件编译、源文件包含、宏替换、重定义行号、错误信息、编译器预留指令

文章目录 一、概述二、格式2.1 条件编译2.2 源文件包含2.3 宏替换2.3.1 语法2.3.2 C标准内置的预定义宏 2.4 重定义行号和文件名2.5 错误信息2.6 编译器预留指令 三、应用场景 C的 Build 可分为4个步骤&#xff1a;预处理、编译、汇编、链接。 预处理就是本文要详细说的宏替换…

基于Red Hat Enterprise Linux 7操作系统的PostgresSql15的备份恢复(实践笔记)

零、前言 本文是基于阿里云ECS服务器进行的实践操作&#xff0c;操作系统版本&#xff1a;Red Hat Enterprise Linux 7 PG数据库版本&#xff1a;PostgresSql 15 PG安装方式&#xff1a;yum 由于本人新接触pg数据&#xff0c;本次也是出于好奇&#xff0c;就对pg数据库的pg_du…

C#,《小白学程序》第五课:队列(Queue)

1 文本格式 /// <summary> /// 《小白学程序》第五课&#xff1a;队列&#xff08;Queue&#xff09; /// 日常生活中常见的排队&#xff0c;软件怎么体现呢&#xff1f; /// 排队的基本原则是&#xff1a;先到先得&#xff0c;先到先吃&#xff0c;先进先出 /// </su…

iOS开发Swift-枚举

枚举&#xff1a;一组相关的值定义了一个共同的类型&#xff0c;使你可以在代码中以类型安全的方式来使用这些值。 1.枚举语法 //枚举成员不会被赋予默认的整型值。成员本身就是完备的值&#xff0c;类型为CompassPoint。 enum CompassPoint {case northcase southcase eastcas…

深度学习8:详解生成对抗网络原理

目录 大纲 生成随机变量 可以伪随机生成均匀随机变量 随机变量表示为操作或过程的结果 逆变换方法 生成模型 我们试图生成非常复杂的随机变量…… …所以让我们使用神经网络的变换方法作为函数&#xff01; 生成匹配网络 培养生成模型 比较基于样本的两个概率分布 …

自学设计模式(简单工厂模式、工厂模式、抽象工厂模式)

使用工厂模式来生产某类对象&#xff08;代码简化且容易维护&#xff0c;类之间有血缘关系&#xff0c;可以通过工厂类进行生产&#xff09;&#xff1b; 简单工厂模式&#xff08;用于创建简单对象&#xff09; 对于简单工厂模式&#xff0c;需要的工厂类只有一个&#xff1…

CSS 属性值计算过程

目录 例子1&#xff0c;确定声明值2&#xff0c;层叠冲突2.1&#xff0c;比较源重要性2.2&#xff0c;比较优先级2.3&#xff0c;比较源次序 3&#xff0c;使用继承4&#xff0c;使用默认值其他 例子 我们来举例说明<h1> 标签最终的样式&#xff1a; <div><h1…

记录一个诡异的bug

将对接oa跳转到会议转写的项目oa/meetingtranslate项目发布到天宫&#xff0c;结果跳转到successPage后报错 这一看就是successPage接口名没对上啊&#xff0c;查了一下代码&#xff0c;没问题啊。 小心起见&#xff0c;我就把successPage的方法请求方式从Post改为Get和POST都…

Linux(基础篇二)

Linux基础篇 Linux基础篇二5. 系统管理5.1 Linux中的进程和服务5.3 systemctl5.4 运行级别CentOS 6CentOS 7 5.5 关机重启命令 Linux基础篇二 5. 系统管理 5.1 Linux中的进程和服务 计算机中&#xff0c;一个正在执行的程序或命令&#xff0c;被叫做“进程”(process) 启动之…

金融客户敏感信息的“精细化管控”新范式

目 录 01 客户信息保护三箭齐发&#xff0c;金融IT亟需把握四个原则‍ 02 制度制约阻碍信息保护的精细化管控 ‍‍‍‍‍‍‍ 03 敏感信息精细化管控范式的6个关键设计 04 分阶段实施&#xff0c;形成敏感信息管控的长效运营的机制 05 未来&#xff0c;新挑战与新机遇并存 …

【无标题】jenkins消息模板(飞书)

这里写目录标题 Jenkins 安装的插件 发送消息到飞书预览 1 &#xff08;单Job&#xff09;预览 2 &#xff08;多Job&#xff0c;概览&#xff09; Jenkins 安装的插件 插件名称作用Rebuilder Rebuilder。 官方地址&#xff1a;https://plugins.jenkins.io/rebuild 安装方式&a…

vue组装模板(侧边栏+顶部+主体)--项目阶段4

目录 一、前言介绍 二、结构解析 三、页面拆分 &#xff08;一&#xff09;页面拆分 1.侧边栏页面&#xff08;固定&#xff09;--Aside.vue 2.顶部页面&#xff08;固定&#xff09;--Header.vue 3.主体页面&#xff08;不固定的&#xff09;--示例用UserView…

【位运算进阶之----左移(<<)】

今天我们来谈谈左移这件事。 ❤️简单来说&#xff0c;对一个数左移就是在其的二进制表达末尾添0。左移一位添一个0&#xff0c;结果就是乘以2&#xff1b;左移两位添两个0&#xff0c;结果就乘以2 ^ 2&#xff1b;左移n位添n个0&#xff0c;结果就是乘以2 ^ n&#xff0c;小心…

延时盲注技术:SQL 注入漏洞检测入门指南

部分数据来源:ChatGPT 环境准备 引言 在网络安全领域中,SQL 注入漏洞一直是常见的安全隐患之一。它可以利用应用程序对用户输入的不恰当处理,导致攻击者能够执行恶意的 SQL 查询语句,进而获取、修改或删除数据库中的数据。为了帮助初学者更好地理解和检测 SQL 注入漏洞,…

小白到运维工程师自学之路 第七十九集 (基于Jenkins自动打包并部署Tomcat环境)2

紧接上文 4、新建Maven项目 clean package -Dmaven.test.skiptrue 用于构建项目并跳过执行测试 拉到最后选择构建后操作 SSH server webExec command scp 192.168.77.18:/root/.jenkins/workspace/probe/psi-probe-web/target/probe.war /usr/local/tomcat/webapps/ /usr/loca…