当前位置: 首页 > news >正文

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的间接调用,效率肯定有所降低。

这种实现方式有一些问题需要注意:

  1. Impl的声明最好设置为struct,原因我也不清楚,因为我用class声明的AImpl(不包含private成员),在Linux上能过,在windows过不去,一直报LINK
    ERROR的错误。我怀疑windows上看不到类的定义时,直接引用类成员函数会有问题。
  2. 一般使用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&#xff0c;指向实现的指针) 是一种减少代码依赖和编译时间的C编程技巧&#xff0c;其基本思想是将一个外部可见类(visible class)的实现细节&#xff08;一般是所有私有的非虚成员&#xff09;放在一个单独的实现类(implementation class)中&…...

rust学习-类型转换

基本类型转换 // 不显示类型转换产生的溢出警告。 #![allow(overflowing_literals)]fn main() {let decimal 65.4321_f32;// 错误&#xff01;不提供隐式转换// let integer: u8 decimal;// 可以显式转换let integer decimal as u8;let character integer as char;println…...

算法通过村第四关-栈青铜笔记|手写栈操作

文章目录 前言1. 栈的基础概要1.1 栈的特征1.2 栈的操作1.3 Java中的栈 2. 栈的实现&#xff08;手写栈&#xff09;2.1 基于数组实现2.2 基于链表实现2.3 基于LinkedList实现 总结 前言 提示&#xff1a;我自己一个人的感觉很好 我并不想要拥有你 除非你比我的独处更加宜人 --…...

Python计算加速利器

迷途小书童的 Note 读完需要 6分钟 速读仅需 2 分钟 1 简介 Python 是一门应用非常广泛的高级语言&#xff0c;但是&#xff0c;长久以来&#xff0c;Python的运行速度一直被人诟病&#xff0c;相比 c/c、java、c#、javascript 等一众高级编程语言&#xff0c;完全没有优势。 那…...

PyTorch 深度学习实践 第10讲刘二大人

总结&#xff1a; 1.输入通道个数 等于 卷积核通道个数 2.卷积核个数 等于 输出通道个数 1.单通道卷积 以单通道卷积为例&#xff0c;输入为&#xff08;1,5,5&#xff09;&#xff0c;分别表示1个通道&#xff0c;宽为5&#xff0c;高为5。假设卷积核大小为3x3&#xff0c…...

Linux特殊指令

目录 1.dd命令 2.mkfs格式化 3.df命令 4.mount实现硬盘的挂载 5.unshare 1.dd命令 dd命令可以用来读取转换并输出数据。 示例一&#xff1a; if表示infile&#xff0c;of表示outfile。这里的/dev/zero是一个特殊文件&#xff0c;会不断产生空白数据。 bs表示复制一块的大…...

MPI之主从模式的一般编程示例

比如&#xff0c;我们可以选举0号进程为master进程&#xff0c;其余进程为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神经网络&#xff08;预测应用&#xff09; - 附代码 文章目录 基于野狗算法优化的BP神经网络&#xff08;预测应用&#xff09; - 附代码1.数据介绍2.野狗优化BP神经网络2.1 BP神经网络参数设置2.2 野狗算法应用 4.测试结果&#xff1a;5.Matlab代码 摘要…...

C语言面向对象的编程思想

面向对象编程 面向对象编程Object-Oriented Programming&#xff0c;OOP&#xff09; 作为一种新方法&#xff0c;其本质是以建立模型体现出来的抽象思维过程和面向对象的方法。模型是用来反映现实世界中事物特征的。任何一个模型都不可能反映客观事物的一切具体特征&#xff0…...

MPI之非阻塞通信中通信完成检测接口简介

在之前的文章中&#xff0c;简单的写了一个非阻塞的通信代码介绍最最基本的使用&#xff1a; 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列&#xff0c;在D2单元格输入公式&#xff1a; -POWER(10,COUNTA($A$2:A2)3)C2 2、选中B1:D10&#xff0c;注意不能宣导A列的合并单元格&#xff0c;进行以下操作&#xff1a; 3、删除辅助列即可 二、COUNTA 第一步&#xff0c;D2建立辅助列&#xf…...

深度学习论文: 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…...

【算法训练-字符串】一 最长无重复子串

废话不多说&#xff0c;喊一句号子鼓励自己&#xff1a;程序员永不失业&#xff0c;程序员走向架构&#xff01;本篇Blog的主题是最长无重复子串或最长无重复子数组&#xff0c;这类题目出现频率还是很高的。 最长无重复子串【MID】 先来看字符串数据结构的题目 题干 解题思…...

【数据结构】手撕顺序表

一&#xff0c;概念及结构 顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构&#xff0c;一般情况下采用数组存储&#xff1b; 在数组上完成数据的增删查改。 1&#xff0c; 静态顺序表&#xff1a;使用定长数组存储元素。 2.&#xff0c;动态顺序表&#xff1…...

景联文科技数据标注:人体关键点标注用途及各点的位置定义

人体关键点标注是一种计算机视觉任务&#xff0c;指通过人工的方式&#xff0c;在指定位置标注上关键点&#xff0c;例如人脸特征点、人体骨骼连接点等&#xff0c;常用来训练面部识别模型以及统计模型。这些关键点可以表示图像的各个方面&#xff0c;例如角、边或特定特征。在…...

typescript基础之never

TypeScript 的 never 类型是一种特殊的类型&#xff0c;它表示的是那些永远不存在的值的类型。例如&#xff0c;一个抛出异常或无限循环的函数的返回值类型就是 never&#xff0c;因为它们永远不会返回任何值。never 类型是所有类型的子类型&#xff0c;也就是说&#xff0c;任…...

电子电路学习笔记之NCP304LSQ37T1G ——超低电流电压检测器

超低电流电压检测器是一种专门用于检测极小电流值的设备。它们常用于电子元件或电路中&#xff0c;用于监测电流的存在和程度。这些检测器通常具有高灵敏度和高精度&#xff0c;能够测量微安级别或更小的电流。 超低电流电压检测器的应用领域广泛&#xff0c;例如电池管理系统…...

【计算机组成原理】一文快速入门,很适合JAVA后端看

作者简介&#xff1a; CSDN内容合伙人、CSDN新星计划导师、JAVA领域优质创作者、阿里云专家博主&#xff0c;计算机科班出身、多年IT从业经验、精通计算机核心理论、Java SE、Java EE、数据库、中间件、分布式技术&#xff0c;参加过国产中间件的核心研发&#xff0c;对后端有…...

10万字智慧政务大数据平台项目建设方案222页[Word]

导读:原文《10万字智慧政务大数据平台项目建设方案222页[Word]》(获取来源见文尾),本文精选其中精华及架构部分,逻辑清晰、内容完整,为快速形成售前方案提供参考。 1.1 项目建设目标 推进市一级政府搭建数字政府建设的规划要求,结合市一级政府“互联网+政务服务”建设…...

Python-主线程控制子线程-4

需求&#xff1a;在Python-主线程控制子线程-3的基础上&#xff0c;新增使用UDP接收指令功能&#xff0c;代替从键盘输入指令 # 修改后的程序&#xff0c;主线程可以获取子线程的结果 import threading import time import queue import tracebackfrom loguru import logger i…...

设计模式二十二:策略模式(Strategy Pattern)

定义一系列算法&#xff0c;将每个算法封装成独立的对象&#xff0c;并使这些对象可互相替换。这使得在运行时可以动态地选择算法&#xff0c;而不必改变使用算法的客户端代码。策略模式的主要目标是将算法的定义与使用分离&#xff0c;使得客户端可以根据需要灵活地选择和切换…...

【c语言】结构体内存对齐,位段,枚举,联合

之前学完结构体&#xff0c;有没有对结构体的大小会很疑惑呢&#xff1f;&#xff1f;其实结构体在内存中存储时会存在内存对齐&#xff0c;捎带讲讲位段&#xff0c;枚举&#xff0c;和联合&#xff0c;跟着小张一起学习吧 结构体内存对齐 结构体的对齐规则: 第一个成员在与结…...

干货丨软件测试行业迎来新时代,AI将成为主流技术?

随着科技日新月异的发展&#xff0c;人工智能正逐渐渗透到我们生活的各方各面&#xff0c;从智能语音助手到自动驾驶汽车、从智能家居到人脸识别技术&#xff0c;AI正以其卓越的智能和学习能力引领着新时代的发展方向。 在这个快速演进的时代中&#xff0c;软件测试领域也受到了…...

MacOS goland go1.21 debug问题

安装dlv brew install dlv 安装之后在终端会显示所在目录 类似/usr/local/Cellar/delve/1.21.0/bin 配置goland 在文件系统中找到goland 右击选择show package contents -> Contents -> plugins -> go 尝试替换 其中对应系统 的 dlv 结果还是不行 然后打开应用gol…...

python 笔记(1)——基础和常用部分

目录 1、print 输出不换行 2、格式化输出字符串 3、浮点数的处理 4、进制转换和ASCII与字符间的转换 5、随机数 6、字符串截取和内置方法 6-1&#xff09;字符串截取 6-2&#xff09;字符串内置方法 7、元组、列表&#xff0c;及其遍历方式 7-1&#xff09;列表常用内…...

kafka架构和原理详解

Apache Kafka 是一个分布式流数据平台,用于高吞吐量、持久性、可扩展的发布和订阅消息。它具有高度的可靠性,被广泛用于构建实时数据流处理、日志收集和数据管道等应用。 基本架构 1. 主题(Topic): 主题是消息的逻辑分类生产者将消息发布到特定的主题中,而消费者可以订阅…...

wsl Ubuntu中非root的普通用户怎么直接执行docker命令

docker需要root权限&#xff0c;如果希望非root用户直接使用docker命令&#xff0c;而不是使用sudo&#xff0c;可以选择将该用户加入到docker用户组。 sudo groupadd docker&#xff1a;添加到groupadd用户组&#xff08;已经有docker用户组&#xff0c;所以可以不用再新增do…...

Web开发模式、API接口、restful规范、序列化和反序列化、drf安装和快速使用、路由转换器(复习)

一 Web开发模式 1. 前后端混合开发模式 前后端混合开发模式是一种开发方式&#xff0c;将前端和后端的开发工作结合在一起&#xff0c;以加快项目的开发速度和 提高协作效率。这种模式通常用于快速原型开发、小型项目或敏捷开发中。在前后端混合开发模式中&#xff0c;前端和…...

<AMBA总线篇> AXI总线协议介绍

目录 01 AXI协议简介 AXI协议特性 AXI协议传输特性 02 AXI协议架构 AXI协议架构 write transaction(写传输) read tramsaction(读传输) Interface and interconnect 典型的AXI系统拓扑 03 文章总结 大家好&#xff0c;这里是程序员杰克。一名平平无奇的嵌入式软件工程…...

一个简单的Python网络爬虫教程

网络爬虫是一种自动获取网页内容的程序&#xff0c;它可以从互联网上的网站中提取数据并进行分析。本教程将带您逐步了解如何使用 Python 构建一个简单的网络爬虫。 注意&#xff1a;在进行网络爬虫时&#xff0c;请遵守网站的使用条款和法律法规&#xff0c;避免对目标网站造…...