C++ Pimpl
Pimpl(Pointer to implementation,指向实现的指针) 是一种减少代码依赖和编译时间的C++编程技巧,其基本思想是将一个外部可见类(visible class)的实现细节(一般是所有私有的非虚成员)放在一个单独的实现类(implementation class)中,而在可见类中通过一个私有指针来间接访问该实现类。
C++虽然不太常提到设计模式,但是对外接口和实现细节的分离仍然是必须的。c++是静态编译语言,他看的就是文件和文件之间的依赖,如果是实例 type a,那么就一定需要include type相关头文件,这样导致一件事情:当多重依赖的时候,很可能基层类的小改动,导致所有包括这个类的大类都需要重新编译
减少编译时间和代码依赖
.h文件中定义了一个类,虽然类中只有一些对外暴露的接口的成员函数,但是类中包含了一些private的成员变量。虽然不影响使用,但是从规范上讲是不合理的。因此需要将接口和实现的细节进行分离。也就是常说的信息隐藏。
对外Release的一个头文件a.h:
class A
{
public:X getX();Y getY();Z getZ();private:X god;Y damn;Z it;
};
头文件形式如下,private成员变量:
#include "X.h"
#include "Y.h"
#include "Z.h"class A
{
public:X getX();Y getY();Z getZ();private:X god;Y damn;Z it;
};
如果直接使用private的方式进行信息隐藏,面临多个问题:
- 别人能看到private成员变量的信息;
- 必须同时给出依赖的X.h,Y.h和Z.h;
- 依赖的头文件和类本身的任何改动都将引发重新编译,即使这个改动本质上是不影响外部调用的。
- 这种方式本质上是一种紧耦合,只是简单的面向对象的封装,隐藏实现细节。
使用依赖类的声明而非定义,这种方式的头文件形式如下:
class X;
class Y;
class Z;class A
{
public:X getX();Y getY();Z getZ();private:X god;Y damn;Z it;
};
可以看到,不用再包含X.h,Y.h和Z.h,当他们发生变化时,A的调用者不必重新编译,阻止了级联依赖的发生,但是别人仍然能看到私有成员信息
使用Impl的代理模式,即A本身只是一个负责对外提供接口的类,真正的实现使用一个AImpl类来代理,接口的实现通过调用Impl类的对应函数来实现,从而实现真正意义上的接口和实现分离
// AImpl.h
struct AImpl
{
public:X getX();Y getY();Z getZ();private:X x;Y y;Z z;
};// A.h
class X;
class Y;
class Z;
struct AImpl;class A
{
public:// 可能的实现: X getX() { return pImpl->getX(); }X getX()Y getY()Z getZ();private:std::tr1::unique_ptr<AImpl> pImpl;
};
任何实现的细节都封装在AImpl类中,所以对于调用端来说是完全不可见的,包括可能用到的成员。其次,只要A的接口没有变化,调用端都不需要重新编译。
但是这种实现也有一个问题,就是多了一个类需要维护,并且每次对A的调用都将是对AImpl的间接调用,效率肯定有所降低。
这种实现方式有一些问题需要注意:
- Impl的声明最好设置为struct,原因我也不清楚,因为我用class声明的AImpl(不包含private成员),在Linux上能过,在windows过不去,一直报LINK
ERROR的错误。我怀疑windows上看不到类的定义时,直接引用类成员函数会有问题。 - 一般使用unique_ptr来包装Impl类,但是使用unique_ptr的时候,接口类的析构函数不能直接定义在类的声明中。因为在类的声明中直接定义析构函数(或者使用=default)的时候,看不到Impl类的实现,也就是看不到Impl类的析构函数,而接口类的析构函数,必须要看unique_ptr成员函数Impl类的析构函数,否则会报can’t
delete an incomplete type错误。- 这个错误其实是一类错误,主要是类的声明不知道类的大小,无论是构造还是析构,都不知道需要为类的对象分配或者回收的内存大小,因此是incomplete type。
- 同时这中前向声明的方式,通常也用于解决循环引用的问题,但是forward declaration方式,被声明的类只能被用于指针,因为作为类的成员变量,必须知道其大小,而声明的Impl类没看到定义,不知道大小,但是指针的大小是固定的。
Impl
weight.h
#ifndef WEIGHT_H
#define WEIGHT_H
#include <memory>
class Weight
{
public:Weight();
private:struct Impl;std::unique_ptr<Impl> m_impl;
};#endif // WEIGHT_H
weight.cpp
#include "Weight.h"
#include <vector>
#include <string>struct Weight::Impl {std::string name;std::vector<double> data;
};Weight::Weight(): m_impl(new Impl())
{}
将所有需要实例化的成员变量创建一个结构体,结构体指针使用unique_ptr管理!!!
但是这种方式在实例化weight的时候会出问题,因为unique_ptr内部默认析构器会对指针类型进行判断如果是不完全的类型会进行报错,为啥会不完全呢,因为编译器默认的析构函数是在头文件隐式内联的,在头文件中当然看不到具体类型
解决办法是:
让析构的时候看到完整类型呗,也就是析构实现的时候看到结构体是完成的,所以将weight的析构函数移到.cpp中
#include "Weight.h"
#include <vector>
#include <string>struct Weight::Impl {std::string name;std::vector<double> data;
};Weight::Weight(): m_impl(new Impl())
{}
Weight::~Weight() {}
也可以使用 ~Weight() = default; 相当于实现使用默认的编译器生成代码
#include "Weight.h"
#include <vector>
#include <string>struct Weight::Impl {std::string name;std::vector<double> data;
};Weight::Weight(): m_impl(new Impl())
{}Weight::~Weight() = default;
那么析构有影响,拷贝构造和赋值操作符呢?
当声明了析构函数,编译器就不会默认生成移动操作符函数,需要显示声明
那么对于下面的
#ifndef WEIGHT_H
#define WEIGHT_H
#include <memory>
class Weight
{
public:Weight();~Weight();Weight(Weight&& rhs) = default;Weight& operator=(Weight&& rhs) = default;
private:struct Impl;std::unique_ptr<Impl> m_impl;
};#endif // WEIGHT_H
因为unique_ptr的原因,我们只能使用默认的移动操作符
然而在
#include <iostream> // std::streambuf, std::cout
#include "Weight.h"
int main () {Weight w;Weight c;w = std::move(c);return 0;
}
报错了,原因是在 移动操作符的默认实现中 会对原有的进行delete处理,这就和析构函数相同了,不完整类型
解决办法就是换个地方,在.h中统一声明
#ifndef WEIGHT_H
#define WEIGHT_H
#include <memory>
class Weight
{
public:Weight();~Weight();Weight(Weight&& rhs);Weight& operator=(Weight&& rhs);
private:struct Impl;std::unique_ptr<Impl> m_impl;
};#endif // WEIGHT_H
#include "Weight.h"
#include <vector>
#include <string>struct Weight::Impl {std::string name;std::vector<double> data;
};Weight::Weight(): m_impl(new Impl())
{}Weight::~Weight() = default;
Weight::Weight(Weight&& rhs) = default;
Weight& Weight::operator=(Weight&& rhs) = default;
为了保证赋值操作符可以正常使用,必须手工自己进行实现
Weight& Weight::operator=(const Weight& rhs) {if (this != &rhs) {*m_impl = *rhs.m_impl;}return *this;
}
使用这种赋值方式,让结构体内部进行赋值,注意的是内存是两块内存,只不过现在内容是一样的了
换成shared_ptr后都不需要了
#ifndef WEIGHT_H
#define WEIGHT_H
#include <memory>
class Weight
{
public:Weight();
private:struct Impl;std::shared_ptr<Impl> m_impl;
};#endif // WEIGHT_H
#include "Weight.h"
#include <vector>
#include <string>struct Weight::Impl {std::string name;std::vector<double> data;
};Weight::Weight(): m_impl(new Impl())
{}
对于unique_ptr他的析构器是智能指针的一部分,因为一开始就可以确定下来,这让编译器可以快速执行代码,这就要求编译时候看到的指针类型是完全的;对于shared_ptr,他的内部析构器不是智能指针的一部分,属于control Block的一部分,所以这也带来的编译器无法优化、减少代码大小
PIMPL的优点:
1)降低模块的耦合。因为隐藏了类的实现,被隐藏的类相当于原类不可见,对隐藏的类进行修改,不需要重新编译原类。
2)降低编译依赖,提高编译速度。指针的大小为(32位)或8(64位),X发生变化,指针大小却不会改变,文件c.h也不需要重编译。
3)接口与实现分离,提高接口的稳定性。
1、通过指针封装,当定义“new C”或"C c1"时 ,编译器生成的代码中不会掺杂X的任何信息。
2、当使用C时,使用的是C的接口(C接口里面操作的类其实是pImpl成员指向的X对象),与X无关,X被通过指针封装彻底的与实现分离。
参考
编译防火墙
相关文章:
C++ Pimpl
Pimpl(Pointer to implementation,指向实现的指针) 是一种减少代码依赖和编译时间的C编程技巧,其基本思想是将一个外部可见类(visible class)的实现细节(一般是所有私有的非虚成员)放在一个单独的实现类(implementation class)中&…...
rust学习-类型转换
基本类型转换 // 不显示类型转换产生的溢出警告。 #![allow(overflowing_literals)]fn main() {let decimal 65.4321_f32;// 错误!不提供隐式转换// let integer: u8 decimal;// 可以显式转换let integer decimal as u8;let character integer as char;println…...
算法通过村第四关-栈青铜笔记|手写栈操作
文章目录 前言1. 栈的基础概要1.1 栈的特征1.2 栈的操作1.3 Java中的栈 2. 栈的实现(手写栈)2.1 基于数组实现2.2 基于链表实现2.3 基于LinkedList实现 总结 前言 提示:我自己一个人的感觉很好 我并不想要拥有你 除非你比我的独处更加宜人 --…...
Python计算加速利器
迷途小书童的 Note 读完需要 6分钟 速读仅需 2 分钟 1 简介 Python 是一门应用非常广泛的高级语言,但是,长久以来,Python的运行速度一直被人诟病,相比 c/c、java、c#、javascript 等一众高级编程语言,完全没有优势。 那…...
PyTorch 深度学习实践 第10讲刘二大人
总结: 1.输入通道个数 等于 卷积核通道个数 2.卷积核个数 等于 输出通道个数 1.单通道卷积 以单通道卷积为例,输入为(1,5,5),分别表示1个通道,宽为5,高为5。假设卷积核大小为3x3,…...
Linux特殊指令
目录 1.dd命令 2.mkfs格式化 3.df命令 4.mount实现硬盘的挂载 5.unshare 1.dd命令 dd命令可以用来读取转换并输出数据。 示例一: if表示infile,of表示outfile。这里的/dev/zero是一个特殊文件,会不断产生空白数据。 bs表示复制一块的大…...
MPI之主从模式的一般编程示例
比如,我们可以选举0号进程为master进程,其余进程为slaver进程 #include "mpi.h" #include <unistd.h> #include <iostream>int main(int argc, char *argv[]) {int err MPI_Init(&argc,&argv);int rank,size;MPI_Comm_r…...
基于野狗算法优化的BP神经网络(预测应用) - 附代码
基于野狗算法优化的BP神经网络(预测应用) - 附代码 文章目录 基于野狗算法优化的BP神经网络(预测应用) - 附代码1.数据介绍2.野狗优化BP神经网络2.1 BP神经网络参数设置2.2 野狗算法应用 4.测试结果:5.Matlab代码 摘要…...
C语言面向对象的编程思想
面向对象编程 面向对象编程Object-Oriented Programming,OOP) 作为一种新方法,其本质是以建立模型体现出来的抽象思维过程和面向对象的方法。模型是用来反映现实世界中事物特征的。任何一个模型都不可能反映客观事物的一切具体特征࿰…...
MPI之非阻塞通信中通信完成检测接口简介
在之前的文章中,简单的写了一个非阻塞的通信代码介绍最最基本的使用: int main(int argc, char *argv[]) {int err MPI_Init(&argc,&argv);int rank,size;MPI_Comm_rank(MPI_COMM_WORLD,&rank);MPI_Comm_size(MPI_COMM_WORLD, &size);…...
Excel:如何实现分组内的升序和降序?
一、POWER 1、构建辅助列D列,在D2单元格输入公式: -POWER(10,COUNTA($A$2:A2)3)C2 2、选中B1:D10,注意不能宣导A列的合并单元格,进行以下操作: 3、删除辅助列即可 二、COUNTA 第一步,D2建立辅助列…...
深度学习论文: Segment Any Anomaly without Training via Hybrid Prompt Regularization
深度学习论文: Segment Any Anomaly without Training via Hybrid Prompt Regularization Segment Any Anomaly without Training via Hybrid Prompt Regularization PDF: https://arxiv.org/pdf/2305.10724.pdf PyTorch代码: https://github.com/shanglianlm0525/CvPytorch Py…...
【算法训练-字符串】一 最长无重复子串
废话不多说,喊一句号子鼓励自己:程序员永不失业,程序员走向架构!本篇Blog的主题是最长无重复子串或最长无重复子数组,这类题目出现频率还是很高的。 最长无重复子串【MID】 先来看字符串数据结构的题目 题干 解题思…...
【数据结构】手撕顺序表
一,概念及结构 顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储; 在数组上完成数据的增删查改。 1, 静态顺序表:使用定长数组存储元素。 2.,动态顺序表࿱…...
景联文科技数据标注:人体关键点标注用途及各点的位置定义
人体关键点标注是一种计算机视觉任务,指通过人工的方式,在指定位置标注上关键点,例如人脸特征点、人体骨骼连接点等,常用来训练面部识别模型以及统计模型。这些关键点可以表示图像的各个方面,例如角、边或特定特征。在…...
typescript基础之never
TypeScript 的 never 类型是一种特殊的类型,它表示的是那些永远不存在的值的类型。例如,一个抛出异常或无限循环的函数的返回值类型就是 never,因为它们永远不会返回任何值。never 类型是所有类型的子类型,也就是说,任…...
电子电路学习笔记之NCP304LSQ37T1G ——超低电流电压检测器
超低电流电压检测器是一种专门用于检测极小电流值的设备。它们常用于电子元件或电路中,用于监测电流的存在和程度。这些检测器通常具有高灵敏度和高精度,能够测量微安级别或更小的电流。 超低电流电压检测器的应用领域广泛,例如电池管理系统…...
【计算机组成原理】一文快速入门,很适合JAVA后端看
作者简介: CSDN内容合伙人、CSDN新星计划导师、JAVA领域优质创作者、阿里云专家博主,计算机科班出身、多年IT从业经验、精通计算机核心理论、Java SE、Java EE、数据库、中间件、分布式技术,参加过国产中间件的核心研发,对后端有…...
10万字智慧政务大数据平台项目建设方案222页[Word]
导读:原文《10万字智慧政务大数据平台项目建设方案222页[Word]》(获取来源见文尾),本文精选其中精华及架构部分,逻辑清晰、内容完整,为快速形成售前方案提供参考。 1.1 项目建设目标 推进市一级政府搭建数字政府建设的规划要求,结合市一级政府“互联网+政务服务”建设…...
Python-主线程控制子线程-4
需求:在Python-主线程控制子线程-3的基础上,新增使用UDP接收指令功能,代替从键盘输入指令 # 修改后的程序,主线程可以获取子线程的结果 import threading import time import queue import tracebackfrom loguru import logger i…...
古戏台构件声学特性的时域有限差分方法【附模型】
✨ 长期致力于时域有限差分法、窑洞、戏台、八字墙、共形技术研究工作,擅长数据搜集与处理、建模仿真、程序编写、仿真设计。 ✅ 专业定制毕设、代码 ✅ 如需沟通交流,点击《获取方式》 (1)曲面共形网格快速生成算法: …...
如何进行TVA仿真引擎的“光照地狱”训练?
重磅预告:本专栏将独家连载系列丛书《智能体视觉技术与应用》部分精华内容,该书是世界首套系统阐述“因式智能体”视觉理论与实践的专著,特邀美国 TypeOne 公司首席科学家、斯坦福大学博士 Bohan 担任技术顾问。Bohan先生师从美国三院院士、“…...
十年以上经验的建站公司推荐|策划强、落地稳的网站制作公司盘点
互联网时代,企业官网已从单纯的信息展示窗口升级为集品牌价值传递、用户体验连接与业务高效转化于一体的核心数字阵地。行业报告显示,优质官网可帮助企业线上转化率提升35%-60%,而低效官网则可能导致潜在客户大量流失。面对市场上众多的网站建…...
3步零基础掌握星露谷物语SMAPI模组加载器:高效管理你的模组世界
3步零基础掌握星露谷物语SMAPI模组加载器:高效管理你的模组世界 【免费下载链接】SMAPI The modding API for Stardew Valley. 项目地址: https://gitcode.com/gh_mirrors/smap/SMAPI SMAPI(Stardew Valley Modding API)是星露谷物语官…...
如何高效使用HiveWE:魔兽争霸III地图制作的完整秘籍
如何高效使用HiveWE:魔兽争霸III地图制作的完整秘籍 【免费下载链接】HiveWE A Warcraft III world editor. 项目地址: https://gitcode.com/gh_mirrors/hi/HiveWE 还在为魔兽争霸III原版编辑器加载缓慢、操作卡顿而烦恼吗?HiveWE作为一款专注于速…...
Claude Mythos Preview首月揪万余漏洞、拦截150万美元电诈,网络安全格局将变?
玻璃翼计划首战告捷A厂的玻璃翼计划首战告捷,Mythos 30天内就挖出1万个致命漏洞,甚至拦截了150万美元电诈。面对雪片式的报告,人类程序员崩溃求饶:「求别挖了,根本修不完啊!」就在刚刚,Anthropi…...
Elsevier-Tracker:5分钟打造您的学术论文审稿进度监控系统
Elsevier-Tracker:5分钟打造您的学术论文审稿进度监控系统 【免费下载链接】Elsevier-Tracker 项目地址: https://gitcode.com/gh_mirrors/el/Elsevier-Tracker 在科研工作者的日常中,论文审稿进度追踪常常成为消耗时间与精力的隐形负担。每天反…...
如何用500KB工具完全替代AWCC:AlienFX Tools终极指南
如何用500KB工具完全替代AWCC:AlienFX Tools终极指南 【免费下载链接】alienfx-tools Alienware systems lights, fans, and power control tools and apps 项目地址: https://gitcode.com/gh_mirrors/al/alienfx-tools 你是否厌倦了Alienware Command Cente…...
终极指南:5步精通开源网页版三国杀无名杀
终极指南:5步精通开源网页版三国杀无名杀 【免费下载链接】noname 项目地址: https://gitcode.com/GitHub_Trending/no/noname 想要随时随地畅玩经典的三国杀卡牌游戏吗?无名杀作为当前最受欢迎的开源网页版三国杀,让你无需下载客户端…...
初创团队如何借助Taotoken以低成本快速验证AI产品创意
🚀 告别海外账号与网络限制!稳定直连全球优质大模型,限时半价接入中。 👉 点击领取海量免费额度 初创团队如何借助Taotoken以低成本快速验证AI产品创意 对于资源有限的初创团队而言,验证一个AI产品创意的核心挑战往往…...
