《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 代码

本章学习代码

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

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

相关文章

超过 50% 的内部攻击使用特权提升漏洞

特权提升漏洞是企业内部人员在网络上进行未经授权的活动时最常见的漏洞&#xff0c;无论是出于恶意目的还是以危险的方式下载有风险的工具。 Crowdstrike 根据 2021 年 1 月至 2023 年 4 月期间收集的数据发布的一份报告显示&#xff0c;内部威胁正在上升&#xff0c;而利用权…

基于SSM的剧本杀预约系统的设计与实现

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

【第三届】:“玄铁杯”RISC-V应用创新大赛(基于yolov5和OpenCv算法 — 智能警戒哨兵)

文章目录 前言 一、智能警戒哨兵是什么&#xff1f; 二、方案流程图 三、硬件方案 四、软件方案 五、演示视频链接 总结 前言 最近参加了第三届“玄铁杯”RISC-V应用创新大赛&#xff0c;我的创意题目是基于 yolov5和OpenCv算法 — 智能警戒哨兵 先介绍一下比赛&#xf…

docker容器配置MySQL与远程连接设置(纯步骤)

以下为ubuntu20.04环境&#xff0c;默认已安装docker&#xff0c;没安装的网上随便找个教程就好了 拉去mysql镜像 docker pull mysql这样是默认拉取最新的版本latest 这样是指定版本拉取 docker pull mysql:5.7查看已安装的mysql镜像 docker images通过镜像生成容器 docke…

大数据HCIE成神之路之数据预处理(1)——缺失值处理

缺失值处理 1.1 删除1.1.1 实验任务1.1.1.1 实验背景1.1.1.2 实验目标1.1.1.3 实验数据解析 1.1.2 实验思路1.1.3 实验操作步骤1.1.4 结果验证 1.2 填充1.2.1 实验任务1.2.1.1 实验背景1.2.1.2 实验目标1.2.1.3 实验数据解析 1.2.2 实验思路1.2.3 实验操作步骤1.2.4 结果验证 1…

【STM32】ADC模数转换器

1 ADC简介 ADC&#xff08;Analog-Digital Converter&#xff09;模拟-数字转换器 ADC可以将引脚上连续变化的模拟电压转换为内存中存储的数字变量&#xff0c;建立模拟电路到数字电路的桥梁 STM32是数字电路&#xff0c;只有高低电平&#xff0c;没有几V电压的概念&#xff…

安装 DevEco Studio 后不能用本地 Node.js 打开

安装 DevEco Studio 后第一次打开时&#xff0c;不能用本地 Node.js 打开 答&#xff1a;因为本地 Node.js 文件夹名字中有空格 Node.js路径只能包含字母、数字、“。”、“_”、“-”、“:”和“V” 解决方法&#xff1a; 1.修改文件夹名称 2.重新下载 注意&#xff1a;找一…

Qt 通过命令行编译程序

前言 从服务器拉代码到编译成可执行文件一个脚本解决问题。使用的项目文件见上一个文章 Qt生成动态链接库并使用动态链接库 脚本代码 为了方便易懂这是一个很简单的Qt编译脚本 call E:\vs2015\VC\vcvarsall.bat x86 rmdir /s /q my-project git clone gitgitee.com:wenbai1…

【CF245H】Queries for Number of Palindromes(字符串区间dp)

Queries for Number of Palindromes - 洛谷 # Queries for Number of Palindromes ## 题面翻译 题目描述 给你一个字符串s由小写字母组成&#xff0c;有q组询问&#xff0c;每组询问给你两个数&#xff0c;l和r&#xff0c;问在字符串区间l到r的字串中&#xff0c;包含多少…

1-3算法基础-标准模板库STL

1.pair pair用于存储两个不同类型的值&#xff08;元素&#xff09;作为一个单元。它通常用于将两个值捆绑在一起&#xff0c;以便一起传递或返回。 #include <iostream> #include <utility> using namespace std; int main() {pair<int, string> person m…

TailwindCSS 多主题色配置

TailwindCSS 多主题色配置 现在大多数网站都支持主题色变换&#xff0c;比如切换深色模式。那么我们该如何进行主题色配置呢&#xff1f; tailwind dark tailwind 包含一个 dark变体&#xff0c;当启用深色模式时&#xff0c;可以为网站设置不同样式 <div class"bg-whi…

ThingWorx 9.2 Windows安装

参考官方文档安装配置 1 PostgreSQL 13.X 2 Java, Apache Tomcat, and ThingWorx PTC Help Center 参考这里安装 数据库 C:\ThingworxPostgresqlStorage 设置为任何人可以full control 数据库初始化 pgadmin4 创建用户twadmin并记录口令password Admin Userpostgres Thin…

漏刻有时百度地图API实战开发(9)Echarts使用bmap.js实现轨迹动画效果

Bmap.js是Echarts和百度地图相结合开发的一款JavaScript API&#xff0c;它可以帮助用户在web应用中获取包括地图中心点、地图缩放级别、地图当前视野范围、地图上标注点等在内的地图信息&#xff0c;并且支持在地图上添加控件&#xff0c;提供包括智能路线规划、智能导航(驾车…

C# WPF上位机开发(通讯协议的编写)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 作为上位机&#xff0c;它很重要的一个部分就是需要和外面的设备进行数据沟通的。很多时候&#xff0c;也就是在这个沟通的过程当中&#xff0c;上…

PyQt下使用OpenCV实现人脸检测与识别

背景&#xff1a; 一 数字图像处理与识别警务应用模型 基于前期所学知识&#xff0c;与公安实践相结合&#xff0c;综合设计数字图像处理与识别警务应用模型,从下列4个研究课题中选择2个进行实验实现&#xff1a;图像增强与复原、人脸检测与识别、虹膜内外圆检测与分割、车牌…

Html转PDF,前端JS实现Html页面导出PDF(html2canvas+jspdf)

Html转PDF&#xff0c;前端JS实现Html页面导出PDF&#xff08;html2canvasjspdf&#xff09; 文章目录 Html转PDF&#xff0c;前端JS实现Html页面导出PDF&#xff08;html2canvasjspdf&#xff09;一、背景介绍二、疑问三、所使用技术html2canvasjspdf 四、展示开始1、效果展示…

C语言----文件操作(一)

一&#xff1a;C语言中文件的概念 对于文件想必大家都很熟悉&#xff0c;无论在windows上还是Linux中&#xff0c;我们用文件去存储资料&#xff0c;记录笔记&#xff0c;常见的如txt文件&#xff0c;word文档&#xff0c;log文件等。那么&#xff0c;在C语言中文件是什么样的存…

threadpool github线程池学习

参考项目 https://github.com/progschj/ThreadPool 源码分析 // 常规头文件保护宏, 避免重复 include #ifndef THREAD_POOL_H #define THREAD_POOL_H// 线程池, 存储线程对象; #include <vector>// 任务队列, 双向都可操作队列, queue 不能删除首个元素 #include <…

微信小程序制作-背单词的小程序制作

微信小程序–背单词的 好久没有发过文章了&#xff0c;但是不代表着我不去学习了喽&#xff0c;以下是我最近做的东西&#xff0c;前端的UI由朋友设计的&#xff0c;目前这个是前端使用的是微信小程序后端是Python的一个轻量型框架&#xff0c;FastApi&#xff0c;嗯&#xff…

MyBatis 四大核心组件之 Executor 源码解析

&#x1f680; 作者主页&#xff1a; 有来技术 &#x1f525; 开源项目&#xff1a; youlai-mall &#x1f343; vue3-element-admin &#x1f343; youlai-boot &#x1f33a; 仓库主页&#xff1a; Gitee &#x1f4ab; Github &#x1f4ab; GitCode &#x1f496; 欢迎点赞…