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

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

本章内容

本章涵盖了一些与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就没有编译时依赖关系,它们的关系是运行时动态创建的。

相关文章:

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

本章内容 本章涵盖了一些与CAPI设计相关的设计模式和惯用法。 “设计模式(Design Pattern)”表示软件设计问题的一些通用解决方案。该术语来源于《设计模式&#xff1a;可复用面向对象软件的基础》&#xff08;Design Patterns: Elements of Reusable Object-Oriented Softwar…...

小程序搜索词优化:小陈运营的秘密武器

大家好&#xff0c;我是小陈&#xff0c;今天要和大家分享一下小程序搜索词优化的经验和技巧。在数字化时代&#xff0c;小程序已经成为许多企业的重要工具&#xff0c;但要让小程序在竞争激烈的市场中脱颖而出&#xff0c;搜索词优化是不可或缺的一环。在本文中&#xff0c;我…...

SpringSecurity 入门

文章目录 Spring Security概念快速入门案例环境准备Spring配置文件SpringMVC配置文件log4j配置文件web.xmlTomcat插件 整合SpringSecurity 认证操作自定义登录页面关闭CSRF拦截数据库认证加密认证状态记住我授权注解使用标签使用 Spring Security概念 Spring Security是Spring…...

【每日一题Day335】LC1993树上的操作 | dfs

树上的操作【LC1993】 给你一棵 n 个节点的树&#xff0c;编号从 0 到 n - 1 &#xff0c;以父节点数组 parent 的形式给出&#xff0c;其中 parent[i] 是第 i 个节点的父节点。树的根节点为 0 号节点&#xff0c;所以 parent[0] -1 &#xff0c;因为它没有父节点。你想要设计…...

FPGA:卷积编码及维特比译码仿真

FPGA&#xff1a;卷积编码及维特比译码仿真 本篇记录一下在FPGA中完成卷积编码和维特比译码的过程&#xff0c;通过代码解释编码的过程和译码的过程&#xff0c;便于理解&#xff0c;同时也方便移植到其他工程中。 1. 准备工作 卷积编译码IP核—convolutionIP核和viterbiIP核…...

MySQL学习笔记4

客户端工具的使用&#xff1a; MySQL&#xff1a; mysql命令行工具&#xff0c;一般用来连接访问mysql的数据。 案例&#xff1a;使用mysql客户端工具连接服务器端&#xff08;用户名&#xff1a;root&#xff1b;密码&#xff1a;123456&#xff09;. [rootmysql-server ~]#…...

JavaFX:窗体显示状态,模态非模态

程序窗体显示一般有3中模式。非模态和模态&#xff0c;其中模态又分为程序模态和窗体模态。 非模态可以理解为窗体之间没有任何限制&#xff0c;可以用鼠标、键盘等工具在窗体间切换。 程序模态是窗体打开后&#xff0c;该程序的所有窗体都被冻结&#xff0c;无法切换&#x…...

C++17中std::filesystem::path的使用

C17引入了std::filesystem库(文件系统库, filesystem library)。这里整理下std::filesystem::path的使用。 std::filesystem::path&#xff0c;文件系统路径&#xff0c;提供了对文件系统及其组件(例如路径、常规文件和目录)执行操作的工具。此path类主要用法包括&#x…...

命令模式简介

概念&#xff1a; 命令模式是一种行为设计模式&#xff0c;它将请求封装成一个对象&#xff0c;从而允许您将不同的请求参数化、队列化&#xff0c;并且能够在不同的时间点执行。通过引入命令对象&#xff08;Command&#xff09;来解耦发送者&#xff08;Invoker&#xff09;…...

Boost序列化指针

Boost.Serialization 还能序列化指针和引用。 由于指针存储对象的地址&#xff0c;序列化对象的地址没有什么意义&#xff0c;而是在序列化指针和引用时&#xff0c;对象的引用被自动地序列化。 代码 #include <boost/archive/text_oarchive.hpp> #include <boost/…...

NIO简单介绍

一、什么是NIO 1、Java NIO全称java non-blocking IO&#xff0c; 是指JDK提供的新API。从JDK1.4开始&#xff0c;Java提供了一系列改进的输入/输出的新特性&#xff0c;被统称为NIO(即New IO)&#xff0c;是同步非阻塞的 2、NIO有三大核心部分: Channel(通道)&#xff0c; Buf…...

linux进程杀不死

项目场景&#xff1a; 虚拟机 问题描述 linux进程杀不死 无反应 原因分析&#xff1a; 进程僵死zombie 解决方案&#xff1a; 进proc或者find命令找到进程所在地址 cat status查看进程杀死子进程...

5分钟带你搞懂RPA到底是什么?RPA能做什么?

一、RPA的定义 RPA&#xff0c;全称Robotic Process Automation&#xff0c;即机器人流程自动化&#xff0c;是一种软件解决方案&#xff0c;能够模拟人类在计算机上执行的操作&#xff0c;以实现重复性、繁琐任务的自动化。它与传统的计算机自动化有所不同&#xff0c;因为它…...

毫米波雷达 TI IWR1443 在 ROS 中进行 octomap 建图

个人实验记录 /mmwave_ti_ros/ros_driver/src/ti_mmwave_rospkg/launch/1443_multi_3d_0.launch <launch><!-- Input arguments --><arg name"device" value"1443" doc"TI mmWave sensor device type [1443, 1642]"/><arg…...

113双周赛

题目列表 2855. 使数组成为递增数组的最少右移次数 2856. 删除数对后的最小数组长度 2857. 统计距离为 k 的点对 2858. 可以到达每一个节点的最少边反转次数 一、使数组成为递增数组的最少右移次数 这题可以直接暴力求解&#xff0c;枚举出每种右移后的数组&#xff0c;将…...

React 全栈体系(九)

第五章 React 路由 一、相关理解 1. SPA 的理解 单页 Web 应用&#xff08;single page web application&#xff0c;SPA&#xff09;。整个应用只有一个完整的页面。点击页面中的链接不会刷新页面&#xff0c;只会做页面的局部更新。数据都需要通过 ajax 请求获取, 并在前端…...

阈值化分割,对灰度级图像进行二值化处理(数字图像处理大题复习 P8)

文章目录 画出表格求出灰度直方图 & 山谷画出结果图 画出表格 有几个0就写几&#xff0c;有几个1就写几&#xff0c;如图 求出灰度直方图 & 山谷 跟前面求灰度直方图的方法一样&#xff0c;找出谷底&#xff0c;发现结果为 4 画出结果图 最终的结果就是&#xf…...

vue3中withDefaults是什么

问: const props withDefaults(defineProps<{// 数据列表lotteryList: { pic: string; name?: string }[];// 中奖idwinId: number;// 抽奖初始转动速度initSpeed: number;// 抽奖最快转动速度fastSpeed: number;// 抽奖最慢转动速度slowSpeed: number;// 基本圈数baseCi…...

Android进阶之路 - 盈利、亏损金额格式化

在金融类型的app中&#xff0c;关于金额、数字都相对敏感和常见一些&#xff0c;在此仅记录我在金融行业期间学到的皮毛&#xff0c;如后续遇到新的场景也会加入该篇 该篇大多采用 Kotlin 扩展函数的方式进行记录&#xff0c;尽可能熟悉 Kotlin 基础知识 兄弟 Blog StringUti…...

工业蒸汽量预测(速通一)

工业蒸汽量预测&#xff08;一&#xff09; 赛题理解1、评估指标2、赛题模型3、解题思路 理论知识1、变量识别2、变量分析3、缺失值处理4、异常值处理5、变量转换6、新变量生成 数据探索1、导包2、读取数据3、查看数据4、可视化数据分布4.1箱型图4.2获取异常数据并画图4.3直方图…...

使用VSCode开发Django指南

使用VSCode开发Django指南 一、概述 Django 是一个高级 Python 框架&#xff0c;专为快速、安全和可扩展的 Web 开发而设计。Django 包含对 URL 路由、页面模板和数据处理的丰富支持。 本文将创建一个简单的 Django 应用&#xff0c;其中包含三个使用通用基本模板的页面。在此…...

从WWDC看苹果产品发展的规律

WWDC 是苹果公司一年一度面向全球开发者的盛会&#xff0c;其主题演讲展现了苹果在产品设计、技术路线、用户体验和生态系统构建上的核心理念与演进脉络。我们借助 ChatGPT Deep Research 工具&#xff0c;对过去十年 WWDC 主题演讲内容进行了系统化分析&#xff0c;形成了这份…...

Java如何权衡是使用无序的数组还是有序的数组

在 Java 中,选择有序数组还是无序数组取决于具体场景的性能需求与操作特点。以下是关键权衡因素及决策指南: ⚖️ 核心权衡维度 维度有序数组无序数组查询性能二分查找 O(log n) ✅线性扫描 O(n) ❌插入/删除需移位维护顺序 O(n) ❌直接操作尾部 O(1) ✅内存开销与无序数组相…...

什么是库存周转?如何用进销存系统提高库存周转率?

你可能听说过这样一句话&#xff1a; “利润不是赚出来的&#xff0c;是管出来的。” 尤其是在制造业、批发零售、电商这类“货堆成山”的行业&#xff0c;很多企业看着销售不错&#xff0c;账上却没钱、利润也不见了&#xff0c;一翻库存才发现&#xff1a; 一堆卖不动的旧货…...

Redis的发布订阅模式与专业的 MQ(如 Kafka, RabbitMQ)相比,优缺点是什么?适用于哪些场景?

Redis 的发布订阅&#xff08;Pub/Sub&#xff09;模式与专业的 MQ&#xff08;Message Queue&#xff09;如 Kafka、RabbitMQ 进行比较&#xff0c;核心的权衡点在于&#xff1a;简单与速度 vs. 可靠与功能。 下面我们详细展开对比。 Redis Pub/Sub 的核心特点 它是一个发后…...

React---day11

14.4 react-redux第三方库 提供connect、thunk之类的函数 以获取一个banner数据为例子 store&#xff1a; 我们在使用异步的时候理应是要使用中间件的&#xff0c;但是configureStore 已经自动集成了 redux-thunk&#xff0c;注意action里面要返回函数 import { configureS…...

在QWebEngineView上实现鼠标、触摸等事件捕获的解决方案

这个问题我看其他博主也写了&#xff0c;要么要会员、要么写的乱七八糟。这里我整理一下&#xff0c;把问题说清楚并且给出代码&#xff0c;拿去用就行&#xff0c;照着葫芦画瓢。 问题 在继承QWebEngineView后&#xff0c;重写mousePressEvent或event函数无法捕获鼠标按下事…...

动态 Web 开发技术入门篇

一、HTTP 协议核心 1.1 HTTP 基础 协议全称 &#xff1a;HyperText Transfer Protocol&#xff08;超文本传输协议&#xff09; 默认端口 &#xff1a;HTTP 使用 80 端口&#xff0c;HTTPS 使用 443 端口。 请求方法 &#xff1a; GET &#xff1a;用于获取资源&#xff0c;…...

RSS 2025|从说明书学习复杂机器人操作任务:NUS邵林团队提出全新机器人装配技能学习框架Manual2Skill

视觉语言模型&#xff08;Vision-Language Models, VLMs&#xff09;&#xff0c;为真实环境中的机器人操作任务提供了极具潜力的解决方案。 尽管 VLMs 取得了显著进展&#xff0c;机器人仍难以胜任复杂的长时程任务&#xff08;如家具装配&#xff09;&#xff0c;主要受限于人…...

保姆级【快数学会Android端“动画“】+ 实现补间动画和逐帧动画!!!

目录 补间动画 1.创建资源文件夹 2.设置文件夹类型 3.创建.xml文件 4.样式设计 5.动画设置 6.动画的实现 内容拓展 7.在原基础上继续添加.xml文件 8.xml代码编写 (1)rotate_anim (2)scale_anim (3)translate_anim 9.MainActivity.java代码汇总 10.效果展示 逐帧…...