《C++20设计模式》学习笔记---原型模式
C++20设计模式
- 第 4 章 原型模式
- 4.1 对象构建
- 4.2 普通拷贝
- 4.3 通过拷贝构造函数进行拷贝
- 4.4 “虚”构造函数
- 4.5 序列化
- 4.6 原型工厂
- 4.7 总结
- 4.8 代码
第 4 章 原型模式
考虑一下我们日常使用的东西,比如汽车或手机。它们并不是从零开始设计的,相反,制造商会选择一个现有的设计方案对其作适当的改进,使其外观区别于以往的设计,然后淘汰老式的方案,开始销售新产品。这是普遍存在的场景,在软件世界中,我们也会遇到类似的情形:有时,相比从零开始创建对象(此时工厂和构造器可以发挥作用),我们更希望使用预先构建好的对象或拷贝或基于此做一些自定义设计。
由此,我们产生了一种想法,即原型模式:一个原型是指一个模型对象,我们对其进行拷贝、自定义拷贝,然后使用它们。原型模式的挑战实际上是拷贝部分,其他一切都很简单。
4.1 对象构建
大多数对象通过构造函数进行构建。但是如果已经有一个完整配置的对象,为ieshme不简单的拷贝该对象而非要重新创建一个相同的对象呢?如果必须使用构造器模式来简化逐段构建对象的过程,那么理解原型模式尤其重要。
我们先看一个简单但可以直接说明对象拷贝的示例:
Contact john{"John Doe", Address{"123 East Dr" , "Londo", 10}};
Contact jane{"Jane Doe", Address{"123 East Dr" , "Londo", 11}};
john和jane工作在同一栋建筑大楼的不同办公室。可能有许多人也在123 East Dr工作,在构建对象时我们想避免重复对该地址信息做初始化。怎么做呢?
原型模式与对象拷贝相关。当然,我们没有通用的方法来拷贝对象,但是可以选择一些可选的对象拷贝方法。
4.2 普通拷贝
如果曾在拷贝一个值和一个其所有成员都是通过值的方式来存储的对象,那么拷贝毫无问题。例如,在之前的示例中,如果Contact和Address定义为:
class Address{public:std::string street;std::string city;int suite;};class Contact{public:std::string name;Address address;};
那么在使用赋值运算符进行拷贝时,绝对不会有问题(string类型拷贝为深拷贝):
void testOrdinaryCopy() {// here is the prototypeContact worker{"", {"123 East Dr", "London", 0}};// make a copy pf prototype and customize itContact john = worker;john.name = "John Doe";john.address.suite = 10;}
但是,在实际应用中,这种按值存储和拷贝的方式较少见。在许多场景中,通常将内部的Address对象作为指针或者引用,例如:
class Contact{public:std::string name;Address* address;~Contact() {delete address;}};
现在有一个很棘手的问题,因为代码Contact jane = john将会拷贝地址指针,所以john和jane以及其他每一个原型拷贝都会共享同一个地址,这绝对不是我们想要的。
4.3 通过拷贝构造函数进行拷贝
避免拷贝指针的最简单的方法时确保对象的所有组成部分(如上面的实例中的Contact和Address)都完整定义了拷贝构造函数。例如如果使用原始指针保存地址,即:
class Contact{public:std::string name;Address* address;~Contact() {delete address;}};
那么,我们需要定义一个拷贝构造函数。在本示例中,实际上有两种方法可以做到这一点。迎头而来的方法看起来像下面这种:
Contact(const Contact& other):name(other.name)/*, address(new Address(*other.address)*/) {address = new Address{other.address->street,other.address->city,other.address->suite}
不幸的是,这种方法并不通用。这种方法在上面的示例中当然没有问题(前提是Address提供了一个初始化其所有成员的构造函数)。但是如果Address的street的成员是由街道名称、门牌号和一些附加信息组成的,那该怎么版?那时,我们又会遇到同样的拷贝问题。
一种明智的做法是,为Address定义拷贝构造函数。在本示例中,Address的拷贝构造函数相当简单(C++ string类型数据实现为深拷贝致使该拷贝构造函数非常简单):
Address(std::string street, std::string city, int suite):street(street), city(city), suite(suite) {}
现在我们可以重写Contact的构造函数中可以重用拷贝构造函数,即:
Contact(const Contact& other):name(other.name), address(new Address(*other.address)) {}
请注意,ReSharper代码生成器在生成拷贝构造函数和移动构造函数的同时,也会生成拷贝赋值函数。在本实例中,拷贝赋值函数定义为:
Contact operator=(const Contact& other) {if (this == &other) {return *this;}name = other.name;address = other.address;return *this;}
【注】上述的拷贝赋值函数存在一定的问题,当我们调用到赋值函数时,并没有为address重新指定新的Address地址。会存在多个对象指向一块Address地址的问题,这个可能不是我们所想见到的。
完成这些函数定义后,我们可以像之前一样构造对象的原型,然后重用它:
void testCopyConstructor() {Contact worker{"",new Address{"123 East Dr", "London", 0}};Contact john = worker;john.name = "john";john.address->suite = 10;}
【注】在上述的测试代码中,虽然使用了 “=”,但是并不会发生异常,这和我们上一个注释说的就有点矛盾了,是什么原因导致的呢?
当对象赋值给另一个对象时,C++会根据情况调用拷贝构造函数或者拷贝赋值函数。如果在赋值操作时对象已经被初始化过,那么会调用拷贝赋值函数。但如果在赋值操作时对象尚未初始化,即对象已经存在,那么会调用拷贝构造函数。这是因为赋值操作需要先创建对象,然后再将值赋给已经存在的对象。因此,这时会调用拷贝构造函数来初始化新对象。
所以,这里虽然使用了 “=”, 但是其调用的是拷贝构造函数,并不会调用拷贝赋值,因此,不会存在问题,我们不妨把测试代码改写如下:
void testCopyConstructor() {Contact worker{"",new Address{"123 East Dr", "London", 0}};Contact john;john = worker;john.name = "john";john.address->suite = 10;}
然后猜想下会发生什么异常呢?
使用当前这种通过拷贝构造函数进行拷贝的方法是有效。使用这种方法唯一不足而且难以解决的问题是,我们为此需要付出额外的工作,以实现拷贝构造函数,移动构造函数,拷贝赋值函数等。诚然,类似于ReSharper代码生成器一类的工具可以为大多数场景快速生成代码,但会产生很多警告。例如,我们编写如下的嗲吗,并且忘记了提供Address类的拷贝赋值函数的实现,会发生什么:
Contact john = worker;
是的, 程序仍然会通过编译。如果提供了拷贝构造函数会更好一些,因为如果在没有定义构造函数的情况下尝试调用构造函数,程序将会出错,然而赋值运算符 “=” 是普遍存在的。即使你没有为赋值运算符提供特殊的定义和实现。
还有一个问题:假设使用类似二级指针的东西(例如 void **)或unique_str呢?即使它们各有独特之处,但此时像ReSharper和Clion这样的工具也不可能生成正确的代码,所以使用工具为这些类型快速生成代码也许不是一个好主意。
4.4 “虚”构造函数
拷贝构造函数使用之处相当有限,并且存在的一个问题是,为了对变量的深度拷贝。我们需要知道变量具体是那种类型。假设ExtendedAddress类继承自Addressl类:
class ExtendedAddress : public Address {public:std::string country;std::string postcode;ExtendedAddress(const std::string& street, const std::string& city, const int suite, const std::string& country,const std::string& postcode):Address(street, city, suite), country(country) {}ExtendedAddress* clone()override {return new ExtendedAddress(street, city, suite, country, postcode);}};
若我们要拷贝一个存在多态性质的变量:
ExtendedAddress ea = ...;
Address& a = ea;
// dow do you deep-copy 'a' ?
这样的做法存在问题,因为我们并不知道变量a的最终派生类型时是么。由于最终派生类引发的问题,以及拷贝构造函数不能是虚函数。因此我们需要采用其他方法来创建对象的拷贝。
首先,我们以Address对象为例,引入一个虚函数clone(),然后,我们尝试:
virtual Address clone() {return Address{street, city, suite};}
不幸的是,这并不能解决继承场景下的问题。请记住,对于派生对象,我们想返回的是ExtendedAddress类型。但上述代码展示的接口将返回类型固定为Address。我们需要是指针形式的多态,因此再次尝试:
virtual Address* clone() {return new Address{street, city, suite};}
现在,我们可以在派生类中做同样的事情,只不过要提供对应的返回类型:
ExtendedAddress* clone()override {return new ExtendedAddress(street, city, suite, country, postcode);}
现在,我们可以安全放心的调用clone()函数,而不必担心对象由于继承体系被切割:
void testVirtualConstructor() {std::cout << __FUNCTION__ <<"() begin.\n\n";ExtendedAddress ea{"123 East Dr", "London", 0, "UK", "SW101EG"};Address& a = ea; //upcastauto cloned = a.clone();printf("\n\nea: %s\n", typeid(ea).name()); // ExtendedAddressprintf("\n\na: %s\n", typeid(a).name()); // ExtendedAddressprintf("\n\ncloned: %s\n", typeid(cloned).name()); // Address*std::cout << __FUNCTION__ <<"() end.\n\n";}
现在,变量cloned的确是一个指向深度拷贝ExtendedAddress对象的指针了。当然,这个指针的类型是Address*,所以,如果我们需要额外的成员,则可以通过dynamic_cast进行转换或者调用某些虚函数。
如果处于某些原因,我们想要使用拷贝构造函数,则clone()接口可以简化为
ExtendedAddress* clone()override {return new ExtendedAddress(*this);}
之后,所有的工作都可以在拷贝构造函数中完成。
使用clone()方法的不足之处是,编译器并不会检查整个继承体系每个类中实现的clone()方法(并且也没有强行进行检查的方法)。例如,如果忘记在ExtendedAddress类中实现clone()方法,示例代纳同样可以通过编译并且正常运行,但当调用clone()方法是, clone()将构造一个Address而不是ExtendedAddress。
4.5 序列化
其他编程语言的设计者也遇到同样的问题,即必须对整个对象显式定义拷贝操作,并很快意识到类需要“普通可序列化”—默认情况下,类应该可以直接写入字符串和流,而不必使用任何额外的注释(最多可能是一个或两个属性)来指定类或其成员。
这与我们正在讨论的问题有关系吗?当然有,如果可以将类对象序列化到文件或内存中,则可以再将其反序列化,并保留包括其所依赖的对象在内的所有信息。这样,我们就不需要在通过显式定义拷贝操作这种方式做处理获得一个在某个对象基础上的新对象。
遗憾的是,与其他语言不同的是,当提到序列化时,C++不提供免费的午餐。我们不能将复杂的对象序列化为文件。为什么不能?在其他编程语言中,编译的二进制文件不仅包括可执行代码,还包括大量的元数据,而序列化是通过一种反射的特性来实现的,目前这个在C++中是不支持的。
如果我们想要序列化,那么就像显式拷贝操作一样,我们需要自己实现它。幸运的是,我们可以使用名为Boost.Serialization的现成的库来解决序列化的问题,而不用费劲的处理和思考序列化std::string的方法。
【注】由于暂时不使用Boost库,序列化就看到这块了,后面有需要在补充…
4.6 原型工厂
如果我们预定义了要拷贝的对象,那么我们会将它们保存在哪里?全局变量中吗?或许吧!事实上,假设我们公司有主办公室和备用办公室,我们可以这样声明全局变量:
Contact main{"", new Address{"123 East Dr", "London", 0}};
Contact aux{"", new Address{"123B East Dr", "London", 0}};
我们可以将这些预定义的对象放在 Contact.h文件中, 任何使用Contact类的人都可以获取这些全局变量并进行拷贝。但更明智的方法是使用某种专用的类来存储原型,并基于所谓的原型,根据需要产生自定义拷贝。这将给我们带来更多的灵活性。例如,我们可以定义工具函数,产生适当初始化的unique_ptr:
class EmployeeFactory {static Contact main;static Contact aux;static std::unique_ptr<Contact> NewEmployee(std::string name,int suite, Contact& proto) {auto result = std::make_unique<Contact>(proto); //这里会调用拷贝构造result->name = name;result->address->suite = suite;return result;}public:static std::unique_ptr<Contact> NewMainOfficeEmployee(std::string name , int suite) {return NewEmployee(name, suite, main);}static std::unique_ptr<Contact> NewAuxMainOfficeEmployee(std::string name, int suite) {return NewEmployee(name, suite, aux);}};
现在可以按如下方式使用:
void testPrototypeFactory() {auto john = EmployeeFactory::NewMainOfficeEmployee("John Doe", 123);auto jane = EmployeeFactory::NewAuxMainOfficeEmployee("Jane Doe", 125);}
为什么要使用工厂呢?考虑这样一种场景:我们从某个原型拷贝得到一个对象,但忘记自定义该对象的某些属性,此时该对象的某些本该有具体参数值的参数将为0或者空字符串。如果使用之前讨论的工厂,我们可以将所有非完全初始化的构造函数声明为私有的,并且将EmployeeFactory声明为friend class。现在,客户将不再得到为完整构建的Contact对象。
4.7 总结
原型模式体现了对对象进行深度拷贝的概念,因此,不必每次都进行完全初始化,而是可以获取一个预定义的对象,拷贝它,稍微修改它,然后独立于原始的对象使用它。
在C++中,有两种方式实现原型模式的方法,它们都需要手动操作:
- 编写正确拷贝原始对象的代码,也就是执行深度拷贝的代码。这项工作可以在拷贝构造函数 / 拷贝赋值运算符或者单独的成员函数中完成。
- 编写支持序列化 / 反序列化的代码,使用序列化 / 反序列化机制,在完成序列化后立即进行反序列化,由此完成复制。该方法会引入额外的开销,是否使用这种方法取决于具体使用场景下的拷贝频率。与使用拷贝构造函数相比,这种方法的唯一优点是可以不受限制地使用序列化功能。
不论选择那种方法,有些工作是必须完成的。如果决定采取上述两种方法的一种。则可采用一些代码生成工具(比如,类似于ReShareper和CLion的集成开发环境)来辅助。
最后,别忘了,如果对所有数据采用按值存储的方式,实际上并不会有问题,只需要operator=就够了。
4.8 代码
本章学习代码
相关文章:
《C++20设计模式》学习笔记---原型模式
C20设计模式 第 4 章 原型模式4.1 对象构建4.2 普通拷贝4.3 通过拷贝构造函数进行拷贝4.4 “虚”构造函数4.5 序列化4.6 原型工厂4.7 总结4.8 代码 第 4 章 原型模式 考虑一下我们日常使用的东西,比如汽车或手机。它们并不是从零开始设计的,相反&#x…...
SpringBootAdmin设置邮件通知
如果你想要在Spring Boot Admin中配置邮件通知,可以按照以下步骤进行操作: 添加邮件通知的依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-mail</artifactId> </dep…...

深度解析IP应用场景API:提升风险控制与反欺诈能力
前言 在当今数字化时代,网络安全和用户数据保护成为企业日益关注的焦点。IP应用场景API作为一种强大的工具,不仅能够在线调用接口获取IP场景属性,而且具备识别IP真人度的能力,为企业提供了卓越的风险控制和反欺诈业务能力。本文将…...

Java连接数据库增删改查-MyBatis
准备工作: 1.创建一个springboot项目,并添加四个依赖 分别是,MyBatis的启动依赖和安装依赖,SQL的依赖,测试依赖,如下: 2.然后创建一张至少两条数据的表 (表可以用各种图形化工具创…...

在国内,现在月薪1万是什么水平?
看到网友发帖问:现在月薪1W是什么水平? 在现如今的情况下,似乎月薪过万这个标准已经成为衡量个人能力的一个标准了,尤其是现在互联网横行的时代,好像年入百万,年入千万就应该是属于大众的平均水平。 我不是…...

【Python网络爬虫入门教程1】成为“Spider Man”的第一课:HTML、Request库、Beautiful Soup库
Python 网络爬虫入门:Spider man的第一课 写在最前面背景知识介绍蛛丝发射器——Request库智能眼镜——Beautiful Soup库 第一课总结 写在最前面 有位粉丝希望学习网络爬虫的实战技巧,想尝试搭建自己的爬虫环境,从网上抓取数据。 前面有写一…...

燕千云汇联易联袂出击:护航医企合规,丝滑内外协作
👉 如想详细了解燕千云医药行业快速实施包(ITFA),可继续阅读详细内容: 文/玉娇龙 一. 医药行业数字化挑战 医药研发从基础研究到最终注册上市的整个生命周期长则需要10多年,短则需要6-7年,在漫长…...
【线性代数与矩阵论】Jordan型矩阵
Jordan型矩阵 2023年11月3日 #algebra 文章目录 Jordan型矩阵1. 代数重数与几何重数2. Jordan块与Jordan标准型2.1 最小多项式与Jordan标准型2.2 两类重要矩阵 3. 矩阵的Jordan分解3.1 Jordan分解的应用 下链 1. 代数重数与几何重数 在对向量做线性变换时,向量空间…...

laravel的ORM 对象关系映射
Laravel 中的 ORM(Eloquent ORM)是 Laravel 框架内置的一种对象关系映射系统,用于在 PHP 应用中与数据库进行交互。Eloquent 提供了一种优雅而直观的语法,使得开发者可以使用面向对象的方式进行数据库查询和操作。 定义模型&…...

049:VUE 引入jquery的方法和配置
第049个 查看专栏目录: VUE ------ element UI 专栏目标 在vue和element UI联合技术栈的操控下,本专栏提供行之有效的源代码示例和信息点介绍,做到灵活运用。 (1)提供vue2的一些基本操作:安装、引用,模板使…...

Qt设置类似于qq登录页面
头文件 #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QWindow> #include <QIcon> #include <QLabel> #include <QMovie> #include <QLineEdit> #include <QPushButton>QT_BEGIN_NAMESPACE namespace Ui { class…...

【GDB】
GDB 1. GDB调试器1.1 前言1.2 GDB编译程序1.3 启动GDB1.4 载入被调试程序1.5 查看源码1.6 运行程序1.7 断点设置1.7.1 通过行号设置断点1.7.2 通过函数名设置断点1.7.3 通过条件设置断点1.7.4 查看断点信息1.7.5 删除断点 1.8 单步调试1.9 2. GDB调试core文件2.1 设定core文件的…...

深入了解Java Duration类,对时间的精细操作
阅读建议 嗨,伙计!刷到这篇文章咱们就是有缘人,在阅读这篇文章前我有一些建议: 本篇文章大概6000多字,预计阅读时间长需要5分钟。本篇文章的实战性、理论性较强,是一篇质量分数较高的技术干货文章&#x…...
Python:核心知识点整理大全5-笔记
目录 2. 使用方法pop()删除元素 3. 弹出列表中任何位置处的元素 4. 根据值删除元素 3 章 列表简介 3.3 组织列表 3.3.1 使用方法 sort()对列表进行永久性排序 3.3.2 使用函数 sorted()对列表进行临时排序 3.3.3 倒着打印列表 3.3.4 确定列表的长度 3.5 小结 2. 使用方…...
预训练(pre-learning)、微调(fine-tuning)、迁移学习(transfer learning)
预训练(pre-learning) 搭建一个网络模型来完成一个特定的图像分类的任务。首先,你需要随机初始化参数,然后开始训练网络,不断调整参数,直到网络的损失越来越小。在训练的过程中,一开始初始化的…...

王道数据结构课后代码题 p149 第8—— 12(c语言代码实现)
目录 8.假设二叉树采用二叉链表存储结构存储,试设计一个算法,计算一棵给定二叉树的所有双分支结点个数。 9.设树B是一棵采用链式结构存储的二叉树,编写一个把树 B中所有结点的左、右子树进行交换的函数。 10.假设二叉树采用二叉链存储结构存储…...

Nginx服务优化以及防盗链
1. 隐藏版本号 以在 CentOS 中使用命令 curl -I http://192.168.66.10 显示响应报文首部信息。 查看版本号 curl -I http://192.168.66.10 1. 修改配置文件 vim /usr/local/nginx/conf/nginx.conf http {include mime.types;default_type application/octet-stream;…...
20231210 随机矩阵和M矩阵
1. 非负矩阵:矩阵元素均非负 定义 7.1.1 设 A ( a i j ) ∈ R m n \boldsymbol{A}\left(a_{i j}\right) \in \mathbb{R}^{m \times n} A(aij)∈Rmn, 如果 a i j ⩾ 0 , i 1 , ⋯ , m ; j 1 , ⋯ , n , a_{i j} \geqslant 0, \quad i1, \cdots, m ; j1, \cd…...

Linux(centos)学习笔记(初学)
[rootlocalhost~]#:[用户名主机名 当前所在目录]#超级管理员标识 $普通用户的标识 Ctrlshift放大终端字体 Ctrl缩小终端字体 Tab可以补全命令 Ctrlshiftc/V复制粘贴 / :根目录,Linux系统起点 ls: #list列出目录的内容,通常用户查看…...
ECharts标题字体大小自适应变化
我们在做自适应Echarts的时候,字体大小在配置项里是如下配置的, title 标题组件,包含主标题和副标题。 以下是常用的对标题的设置: title:{//设置图表的标题text:"主标题",link:"baidu.com", //设置标题超链接target:"self",...

CMake基础:构建流程详解
目录 1.CMake构建过程的基本流程 2.CMake构建的具体步骤 2.1.创建构建目录 2.2.使用 CMake 生成构建文件 2.3.编译和构建 2.4.清理构建文件 2.5.重新配置和构建 3.跨平台构建示例 4.工具链与交叉编译 5.CMake构建后的项目结构解析 5.1.CMake构建后的目录结构 5.2.构…...

MySQL 8.0 OCP 英文题库解析(十三)
Oracle 为庆祝 MySQL 30 周年,截止到 2025.07.31 之前。所有人均可以免费考取原价245美元的MySQL OCP 认证。 从今天开始,将英文题库免费公布出来,并进行解析,帮助大家在一个月之内轻松通过OCP认证。 本期公布试题111~120 试题1…...

C++ 求圆面积的程序(Program to find area of a circle)
给定半径r,求圆的面积。圆的面积应精确到小数点后5位。 例子: 输入:r 5 输出:78.53982 解释:由于面积 PI * r * r 3.14159265358979323846 * 5 * 5 78.53982,因为我们只保留小数点后 5 位数字。 输…...
AI编程--插件对比分析:CodeRider、GitHub Copilot及其他
AI编程插件对比分析:CodeRider、GitHub Copilot及其他 随着人工智能技术的快速发展,AI编程插件已成为提升开发者生产力的重要工具。CodeRider和GitHub Copilot作为市场上的领先者,分别以其独特的特性和生态系统吸引了大量开发者。本文将从功…...

tree 树组件大数据卡顿问题优化
问题背景 项目中有用到树组件用来做文件目录,但是由于这个树组件的节点越来越多,导致页面在滚动这个树组件的时候浏览器就很容易卡死。这种问题基本上都是因为dom节点太多,导致的浏览器卡顿,这里很明显就需要用到虚拟列表的技术&…...
Device Mapper 机制
Device Mapper 机制详解 Device Mapper(简称 DM)是 Linux 内核中的一套通用块设备映射框架,为 LVM、加密磁盘、RAID 等提供底层支持。本文将详细介绍 Device Mapper 的原理、实现、内核配置、常用工具、操作测试流程,并配以详细的…...

Reasoning over Uncertain Text by Generative Large Language Models
https://ojs.aaai.org/index.php/AAAI/article/view/34674/36829https://ojs.aaai.org/index.php/AAAI/article/view/34674/36829 1. 概述 文本中的不确定性在许多语境中传达,从日常对话到特定领域的文档(例如医学文档)(Heritage 2013;Landmark、Gulbrandsen 和 Svenevei…...

华为OD机考-机房布局
import java.util.*;public class DemoTest5 {public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别while (in.hasNextLine()) { // 注意 while 处理多个 caseSystem.out.println(solve(in.nextLine()));}}priv…...

基于Springboot+Vue的办公管理系统
角色: 管理员、员工 技术: 后端: SpringBoot, Vue2, MySQL, Mybatis-Plus 前端: Vue2, Element-UI, Axios, Echarts, Vue-Router 核心功能: 该办公管理系统是一个综合性的企业内部管理平台,旨在提升企业运营效率和员工管理水…...
Python Einops库:深度学习中的张量操作革命
Einops(爱因斯坦操作库)就像给张量操作戴上了一副"语义眼镜"——让你用人类能理解的方式告诉计算机如何操作多维数组。这个基于爱因斯坦求和约定的库,用类似自然语言的表达式替代了晦涩的API调用,彻底改变了深度学习工程…...