《C++API设计》读书笔记(3):模式

news2024/11/19 4:29:25

本章内容

本章涵盖了一些与C++API设计相关的设计模式惯用法

“设计模式(Design Pattern)”表示软件设计问题的一些通用解决方案。该术语来源于《设计模式:可复用面向对象软件的基础》(Design Patterns: Elements of Reusable Object-Oriented Software

本书不会涵盖所有模式,只讨论一些和API设计有关的。
还会涉及一些C++的惯用法,它们并非是真正的通用设计模式,但却是C++API设计的重要技巧。
本章讨论的技巧如下:

1. Pimpl

Pimpl 意思是 Pointer to Implementation,指向实现的指针。
(Pimpl 不是严格意义上的“设计模式”,而是受制于C++语法特定限制的变通方案,可以看作是 桥接 设计模式的一种特例)

它主要解决的是C++语法中的一个问题:类的private成员其实只在内部使用,但是在定义时还需要写在.h文件中公开。因此,Pimpl 的做法是只在.h中定义一个指向实现的指针,那就可以将一些private成员放在.cpp中的实现中,这样就在.h文件中隐藏了private成员。
举例:
在这里插入图片描述

使用Pimpl的方法

举例代码:
.h文件中:

class AutoTimer
{
public:
	explicit AutoTimer();
	~AutoTimer();
private:
	class Impl;
	Impl* mImpl;
}

随后,在.cpp中可以定义Impl类并实现具体的逻辑。

一个值得考虑的设计问题是:Impl类中放置多少逻辑?有以下选择:

  1. 仅私有变量
  2. 私有变量+私有方法
  3. 公有类的所有方法。(而共有类的方法只是对Impl类中方法的简单包装)

每种选择都适应于不同情况。一般情况下推荐 2 。

另外,使用 Pimpl 时需要注意:

  1. virtual 函数不能放在Impl类中,否则公有类的子类无法继承。
  2. Impl类可能还需要一个公有类的指针以方便其调用公有类的方法。

使用Pimpl的类的复制

使用 Pimpl 的类无法进行默认的复制,因为复制出的对象会和原对象指向同一个Impl类变量。
这个问题有两种方法解决:

  1. 禁止复制
  2. 显示定义复制语义

Pimpl与智能指针

使用 Pimpl 时容易犯错的一点是:构造时忘记分配它,析构时忘记销毁它。
为此,可以采用 智能指针 或者 作用域指针。

Pimpl的优点

  • 信息隐藏。
  • 降低耦合
  • 加速编译
  • 更好的二进制兼容性。(就算Impl类实现发生变化,公用类的对象也不会改变二进制数据)
  • 惰性分配(可以选择只在需要时分配)

Pimpl的缺点

  • 额外的分配与销毁Impl类对象会增加性能开销
  • 给开发者带来了不便:很多函数调用时需要加上mImpl->,如果Impl类需要调用公有类的方法也需要通过指针。
  • 编译器不能再检查constImpl类的成员更改,公有类的const无法检查出。

2. 单例

“单例”设计模式确保一个类仅存在一个实例,并提供唯一的全局访问点。

它可以看作是一种更优雅的全局变量,但是相比全局变量有一些优点:

  • 确保这个类只能创建一个实例。
  • 控制对象的分配与销毁。
  • 可以支持线程安全。
  • 避免污染全局命名空间。

其基本实现很简单,而本篇重点讨论的是:

  • 如何更健壮地实现。
  • 它也有些缺点,但很多人都有滥用单例的趋向。为此这里也提供一些替代方法。

在C++中实现单例

其基本实现很简单:

class Singleton 
{
public:
	static Singleton &GetInstance();
};
Singleton &Singleton::GetInstance()
{
	static Singleton instance;
	return instance;
}

有一些做法可以增加健壮性:

  • 声明私有默认构造函数:防止用户创建新的实例。
  • 声明私有复制构造函数和赋值操作:防止用户复制。
  • 声明私有析构函数:防止用户删除。
  • GetInstance()返回引用而非指针:防止用户删除。

单例的线程安全

上面的 GetInstance() 并非线程安全的。

常规的处理方式是加互斥锁。但这样会增加开销。
要优化此类激进的加锁行为,可以采用DCLP(Double Check Locking Pattern),即加互斥锁前先判断instance是否存在。
但是DCLP不能保证任何编译器和处理器下都能正常工作。

所以,也许你不应该尝试保证GetInstance()是线程安全的(毕竟对使用C++这样对并发缺乏内在支持的语言来说,实现线程安全总会遇到这些困难)。如果你真的需要它线程安全并且性能最高,可以考虑避免惰性实例化模型(即不要在需要他时再实例化),比如:

  1. 静态初始化:在cpp文件中main函数调用之前调用GetInstance()
  2. 显式API初始化:在一开始就调用GetInstance(),调用时可以加互斥锁。而GetInstance()的内部就不用加互斥锁了。

替代方案:依赖注入

初始化时传入需要的实例的指针,而不是内部再使用GetInstance()获得实例。

替代方案:单一状态

假设状态的初始化不需要控制,或者不需要使用单例对象存储,那么就可以使用“单一状态”,即:
类本身不保持是单例,但是其所有成员(或者说“状态”)都是static。

替代方案:会话上下文

《设计模式》作者指出,单例有可能导致拙劣的设计。使用时候需要思考,“单例”是否真的是正确的模式?

需求是会变的,未来有些对象可能会需要支持多个实例。

因此,需要尽早考虑引入 “会话(session)” 或 “上下文(context)” 的概念。这是在强调:使用单一的实例维护所有相关的状态,而非使用多个单例

3. 工厂模式

工厂模式是关于创建的设计模式,本质上是构造函数的泛化,可以回避C++构造函数的限制:

  • 没有返回值。这样无法返回错误等其他信号。
  • 命名限制。这样相同参数的构造函数只能有一个。
  • 静态绑定创建。这样无法在运行时动态决定类型。
  • 不允许虚构造函数。限制同上。

从使用层面上看,工厂方法仅是一个普通的方法,调用时返回对应类的实例:

class RendererFactory
{
public:
	IRenderer* CreateRenderer(string type)
}

但这里的 IRenderer 是一个抽象基类(Abstract Base Class,简称ABC)。抽象基类是包含纯虚函数的类,不能被实例化。(另外要注意,抽象基类的析构函数需要声明为虚的)。

这样,用户可以使用参数动态决定要创建的类型。

另外作为扩展,可以让工厂类提供注册函数,这样用户可以自己添加新的类型

static void RegisterRender(string type, CreateCallback cb);

4. API包装器

基于另一组API来包装接口是一项常见的API设计任务。比如,你在维护一个遗留的代码库,相比重构代码,你更愿意封装一套新的,更简洁的API,以隐藏所有的底层遗留代码。

下面,按照包装器层和原始接口的差异程度递增地划分:

4.1 代理(Proxy)

一对一地,将函数调用转发到具有相同形式的另一个接口。

案例:

  • 实现原始对象的惰性实例。
  • 实现对原始对象的访问控制。
  • 支持 “调试” 模式或者 “演习(DryRun)” 模式。
  • 保证原始对象的线程安全
  • 可以让多个代理对象共享相同的原始对象。
  • 应对原始对象将来被修改的情况。

4.2 适配器

一对一地,将接口转换为一个兼容但是不完全相同的另一个接口。

优点:

  • 可以转换数据类型。
  • 强制API始终保持一致性。
  • 包装API的依赖库

4.3 外观模式

外观模式能够为一组类提供简化的接口。它实际上定义了一个更高层次的接口,使得底层类更易于使用且对用户隐藏。
(一个例子:用一个“酒店助手类”简化了“预定房间”、“预定晚餐”、“预定出租车”等事务。)

用途:

  • 隐藏遗留代码。
  • 创建更便捷的API。
  • 支持简化功能或替代功能的API。

5. 观察者模式

观察者模式为了解决这样的问题:
实现复杂的任务通常需要多个对象一起合作完成。为了让A可以调用B,较为简单的方法就是A.cpp包含B.h,但是这样产生了编译时依赖,迫使想要复用A时也必须引入B。

观察者模式就是 “发布/订阅” 范式的一个具体实例。

实现观察者模式的典型做法是引入两个概念:

  • Subject,主体,也就是发布者。
  • Observer,观察者,也就是订阅者。

代码上,Subject仅知道Observer接口类即IObserver,他将维护IObserver列表

class IObserver
{
	virtual void Update() = 0;
}

class ISubject
{
std::vector<IObserver*> ObserverList
}

随后,观察者通过继承IObserver来实现观察者对象。

然后,在使用时,Subject就可以订阅(Subscribe)若干Observer,并在需要的时候通知(Notify)它们。

这样,Subject和Observer就没有编译时依赖关系,它们的关系是运行时动态创建的。

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

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

相关文章

【查缺补漏 女娲补天】2023平安

秋招了&#xff0c;只根据自己的情况记录&#xff0c;大概率不会很全。标题是我觉得的重点。既搬砖也搬博客。 Telnet协议 远程登录和管理网路设备的标准协议TCP传输层之上&#xff1a;应用层工作模型&#xff1a;C/S模式&#xff08;client/server&#xff09;服务端端口号默…

LeNet-5

目录 一、知识点 二、代码 三、查看卷积层的feature map 1. 查看每层信息 ​2. show_featureMap.py 背景&#xff1a;LeNet-5是一个经典的CNN&#xff0c;由Yann LeCun在1998年提出&#xff0c;旨在解决手写数字识别问题。 一、知识点 1. iter()next() iter()&#xff1a;…

【操作系统】聊聊C10K

什么是C10K问题 C10K 就是 Client 10000 问题&#xff0c;即“在同时连接到服务器的客户端数量超过 10000 个的环境中&#xff0c;即便硬件性能足够&#xff0c; 依然无法正常提供服务。 其实说白了就是并发请求1W个请求 同时进行连接服务端&#xff0c;服务端可以支撑服务。…

Linux系统之安装uptime-kuma服务器监控面板

Linux系统之安装uptime-kuma服务器监控面板 一、uptime-kuma介绍1.1 uptime-kuma简介1.2 uptime-kuma特点 二、本次实践环境介绍2.1 环境规划2.2 本次实践介绍2.3 环境要求 三、检查本地环境3.1 检查本地操作系统版本3.2 检查系统内核版本3.3 检查系统是否安装Node.js 四、部署…

post更新,put相当于删除重新增一条

索引数据 //删除后新增 PUT my_dynamic_temp/_doc/1 { “name”:“test”, “class”:“1204” } //覆盖更新 POST my_dynamic_temp/_update/1 { “doc”: { “name”:“test”, “class”:“1203”, “pernum”:“998” } }

springboot 集成mybatis-plus的使用

一、在spring boot中配置mybatis-plus 1、创建一个spring boot项目&#xff0c;注意勾选mysql 2、在pom.xml文件中添加mybatis-plus的依赖包 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0&qu…

瑞芯微RK3568:烧录系统

烧录系统 文章目录 烧录系统windowsLinuxupgrade_tool 工具烧写烧写 update.img擦除操作使用 rkflash.sh 脚本烧写 编译Linux_SDK后得到多个镜像文件 windows Windows 下通过瑞芯微开发工具&#xff08;RKDevTool&#xff09; 来烧写镜像。 Loader parameter uboot …

狂神docker

狂神说 docker 参考文章 -----docker 概述 docker 为什么会出现&#xff1f;–环境部署麻烦&#xff0c;两套环境&#xff08;开发-运维&#xff09; 我的电脑可以运行&#xff0c;到你那就不可用。 开发即运维–开发打包部署上线一条龙 环境配置十分麻烦&#xff0c;机器部署…

Spring Social微信登录

微信登录的appId获得可在微信开放平台申请&#xff0c;以下用测试号 1、完成WeixinProperties 用测试账号登录 public class WeixinProperties {private String appId "wxd99431bbff8305a0";private String appSecret "60f78681d063590a469f1b297feff3c4&q…

基于SSM+Vue的医学生在线学习交流平台

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用Vue技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

【数字人】1、SadTalker | 使用语音驱动单张图片合成视频(CVPR2023)

Sad Talker&#xff1a;使用一张图片和一段语音来生成口型和头、面部视频 论文&#xff1a;SadTalker: Learning Realistic 3D Motion Coefficients for Stylized Audio-Driven Single Image Talking Face Animation 代码&#xff1a;https://github.com/Winfredy/SadTalker …

Linux命令200例:dip用于用户与远程主机建立通信连接

&#x1f3c6;作者简介&#xff0c;黑夜开发者&#xff0c;CSDN领军人物&#xff0c;全栈领域优质创作者✌。CSDN专家博主&#xff0c;阿里云社区专家博主&#xff0c;2023年6月csdn上海赛道top4。 &#x1f3c6;数年电商行业从业经验&#xff0c;历任核心研发工程师&#xff0…

Stable DIffusion 炫酷应用 | AI嵌入艺术字+光影光效

目录 1 生成AI艺术字基本流程 1.1 生成黑白图 1.2 启用ControlNet 参数设置 1.3 选择大模型 写提示词 2 不同效果组合 2.1 更改提示词 2.2 更改ControlNet 2.2.1 更改模型或者预处理器 2.2.2 更改参数 3. 其他应用 3.1 AI光影字 本节需要用到ControlNet&#xff0c;可…

6.前端·新建子模块与开发(常规开发)

文章目录 学习资料常规开发创建组件与脚本菜单创建-新增自定义图标菜单创建-栏目创建 学习资料 https://www.bilibili.com/video/BV13g411Y7GS?p12&vd_sourceed09a620bf87401694f763818a31c91e 常规开发 创建组件与脚本 首先新建前端的目录结构&#xff0c;属于自己业…

CTF 全讲解:[SWPUCTF 2022 新生赛]webdog1__start

文章目录 参考环境题目learning.php信息收集isset()GET 请求查询字符串全局变量 $_GET MD5 绕过MD5韧性脆弱性 md5()弱比较隐式类型转换字符串连接数学运算布尔判断 相等运算符 MD5 绕过科学计数法前缀 0E 与 0e绕过 start.php信息收集头部检索 f14g.php信息收集 探秘 F1l1l1l1…

Springboot 实践(18)Nacos配置中心参数自动刷新测试

前文讲解了Nacos 2.2.3配置中心的服务端的下载安装&#xff0c;和springboot整合nacos的客户端。Springboot整合nacos关键在于使用的jar版本要匹配&#xff0c;文中使用版本如下&#xff1a; ☆ springboot版本: 2.1.5.RELEASE ☆ spring cloud版本 Greenwich.RELEASE ☆ sp…

Python 算数运算符

视频版教程 Python3零基础7天入门实战视频教程 Python支持所有的基本算术运算符&#xff0c;这些算术运算符用于执行基本的数学运算&#xff0c;如加、减、乘、除和求余等。下面是7个基本的算术运算符。 以下&#xff0c;假设变量a为10&#xff0c;变量b为21&#xff1a; 实…

OpenCV之YOLOv3目标检测

&#x1f482; 个人主页:风间琉璃&#x1f91f; 版权: 本文由【风间琉璃】原创、在CSDN首发、需要转载请联系博主&#x1f4ac; 如果文章对你有帮助、欢迎关注、点赞、收藏(一键三连)和订阅专栏哦 目录 前言 一、预处理 1.获取分类名 2.获取输出层名称 3.图像尺度变换 二…

【JavaSE笔记】初识Java

一、前言 Java是一种非常优秀的程序设计语言&#xff0c;它具有令人赏心悦目的语法和易于理解的语义。 本文将通过一个简单的Java程序&#xff0c;介绍Java的一些基础内容。 二、Java基本结构 1、简单的Java程序 从最简单的一个Java程序开始逐渐了解Java语言。 以下是一段…

数学建模——微分方程介绍

一、基础知识 1、一阶微分方程 称为一阶微分方程。y(x0)y0为定解条件。 其常规求解方法&#xff1a; &#xff08;1&#xff09;变量分离 再两边积分就可以求出通解。 &#xff08;2&#xff09;一阶线性求解公式 通解公式&#xff1a; 有些一阶微分方程需要通过整体代换…