35:考虑virtual函数以外的其他选择

news2024/12/23 23:48:25

 假设你正在写一个视频游戏软件,你打算为游戏内的人物设计一个继承体系,剧中人物被伤害或因其他因素而降低健康状态的情况并不罕见。你因此决定提供一个成员函数healthValue,它会返回一个整数,表示人物的健康程度。

由于不同的人物可能以不同的方式计算他们的健康指数,将headthValue声明为virtual似乎是个再明白不过的做法:

class GameCharacter {
public:
    virtual int healthValue() const;
    //返回人物的健康指数,derived vlass可重新定义它。
    //...
};

healthValue并未被声明为pure virtual,这暗示我们将会有个计算健康指数的缺省算法。

这的确是再明白不过的设计,但从某个角度说却反而成了它的弱点。

让我们考虑其他一些解法。

一、藉由non-virtual interface手法实现Template Method模式

我们将从一个有趣的思想流派开始,这个流派主张virtual函数应该几乎总是private。

这个流派的拥护者建议,较好的设计是保留headthValue为public成员函数 ,但让它成为non-virtual,并调用一个private virtual函数(例如doHealthValue)进行实际工作:

class GameCharacter {
public:
    int healthValue() const//derived class不重新定义它
    {
        //...//做一些事前工作
        int retVal = doHealthValue();//作真正工作
        //...//做一些事后工作
        return retVal;
     }
    //...
private:
    virtual int doHealthValue() const//derived class可重新定义它
    {
        //...//缺省算法,计算健康指数
    }
};

这一基本设计,也就是“令客户通过public non-virtual成员函数间接调用private virtual函数”,称为non-virtual interface(NVI)手法。

它是所谓template method设计模式(与C++ template并无关联)的一个独特表现形式。

将这个non-virtual函数(healthValue)称为virtual函数的外覆器(wrapper)。

(一)NVI手法的优点 

NVI手法的一个优点隐身在上述代码注释“做一些事前工作”和“做一些事后工作”之中。那些注释用来告诉你当时的代码保证在“virtual函数进行真正工作之前和之后”被调用。

这意味外覆器确保得以在一个virtual函数被调用之前设定好适当场景,并在调用结束之后清理场景。

“事前工作”可以包括锁定互斥器、制造运转日志记录项、验证class约束条件、验证函数先决条件等。

“事后工作”可以包括互斥器解除锁定、验证函数的事后条件、再次验证class的约束条件等。

若你让客户直接调用virtual函数,就没有任何好办法可以做这些事。

(二)NVI手法注意 

有件事实可能会妨碍你跃跃欲试的心:NVI手法涉及在derived class内重新定义private virtual函数。

“重新定义virtual函数”表示某些事“如何”被完成,“调用virtual函数”则表示它“何时”被完成。这些事情都是各自独立互不相干的。

NVI手法允许derived class重新定义virtual函数,从而赋予它们“如何实现机能”的控制能力,但base class保留诉说“函数何时被调用”的权利。一开始这些听起来似乎诡异,但C++的这种“derived class可重新定义继承而来的private virtual函数”的规则完全合情合理。

在NVI手法下其实没有必要让virtual函数一定得是private。某些class继承体系要求derived class在virtual函数的实现内必须调用其base class的对应兄弟,而为了让这样的调用合法,virtual函数必须是protected,不能是private。有时virtual函数甚至一定得是public(例如具备多态性质的base class的析构函数),这边一来就不能实施NVI手法了。

二、藉由function pointer实现Strategy模式

NVI手法对public virtual函数而言是一个有趣的替代方案,但从某种设计角度来看,它只比窗饰花样更强一些。毕竟我们还是使用virtual函数来计算每个人的健康指数。

另一个更戏剧性的设计主张“人物健康指数的计算与人物类型无关”,这样的计算完全不需要“人物”这个成分。

例如,我们可能会要求每个人物的构造函数接受一个指针,指向一个健康计算函数,而我们可以调用该函数进行实际计算:

class GameCharacter;//前置声明
//以下函数是计算健康指数的缺省算法
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter {
public:
    typedef int (*HealthCalcFunc)(const GameCharacter&);
    explicit GameCharacter(HealthCalcFunc hcf=defaultHealthCalc):healthFunc(hcf){}
    int healthValue() const//derived class不重新定义它
    {
        return healthFunc(*this);
     }
    //...
private:
    HealthCalcFunc healthFunc;
};

这个做法是常见的Strategy设计模式的简单应用。拿它和“根基于GameCharacter继承体系内的virtual函数”的做法比较,它提供了某些有趣弹性:

(一)Strategy设计模式提供的弹性

1.同一人物类型的不同实体可以有不同的健康计算函数 

例如:

class EvilBadGuy : public GameCharacter {
public:
    explicit EvilBadGuy(HealthCalcFunc hcf=defaultHealthCalc)
        :GameCharacter(hcf){/*...*/ }
    //...
};
int loseHealthQuickly(const GameCharacter&);//健康指数计算函数1
int loseHealthSlowly(const GameCharacter&);//健康指数计算函数2
EvilBadGuy ebg1(loseHealthQuickly);//相同类型的人物搭配
EvilBadGuy ebg2(loseHealthSlowly);//不同的健康计算方式

 2.某已知人物的健康指数计算函数可在运行期变更

例如,GameCharacter可提供一个成员函数setHealthCalculator,用来替换当前的健康指数计算函数。

换句话说,“健康指数计算函数不再是GameCharacter继承体系内的成员函数”这一事实意味着,这些计算函数并未特别访问“即将被计算健康指数”的那个对象的内部成分。

例如,defaultHealthCalc并未访问EvilBadGuy的non-public成分。

若人物的健康可存粹根据人物public接口得来的信息加以计算,这就没有任何问题,但如果需要non-public信息进行精确计算,就有问题了。实际上,任何时候当你将class内的某个机能(也许取道自某个成员函数)替换为class外部的某个等价机能(也许取道自某个non-member non-friend函数或另一个class的non-friend成员函数),这都是潜在争议点。

一般而言,唯一能够解决“需要以non-member函数访问class的non-public成员”的办法就是:弱化class的封装。

例如,class可声明那个non-member函数为friend,或是为其实现的某一部分提供public访问函数(其他部分则宁可隐藏起来)。运用函数指针替换virtual函数,其优点(像是“每个对象可各自拥有自己的健康计算函数”和“可在运行期改变计算函数”)是否足以弥补缺点(例如可能必须降低GameCharacter封装性),是你必须根据每个设计情况的不同而抉择的。 

三、藉由tr1::function完成Strategy模式

一旦习惯了template以及它们对隐式接口的使用,基于函数指针的做法看起来便过分苛刻而死板了。

为什么要求“健康指数的计算”必须是个函数,而不能是某种“像函数的东西”(例如函数对象)呢?如果一定得是函数,为什么不能够是个成员函数?为什么一定得返回int而不是任何可被转换为int的类型呢? 

若我们不再使用函数指针(如前例的healthFunc),而是改用一个类型为tr1::function的对象,这些约束就全都挥发不见了。这样的对象可持有(保存)任何可调用物(callable entity,也就是函数指针、函数对象或成员函数指针),只要其签名式兼容于需求端。

(一)tr1::function完成Strategy模式的实现 

以下将刚才的设计改为使用tr1::function:

class GameCharacter;//前置声明
//以下函数是计算健康指数的缺省算法
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter {
public:
    //HealthCalcFunt可以是任何“可调用物”,可被调用并接受
    //任何兼容于GameCharacter之物,返回任何兼容于int的东西
    typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;
    explicit GameCharacter(HealthCalcFunc hcf=defaultHealthCalc):healthFunc(hcf){}
    int healthValue() const
    {
        return healthFunc(*this);
     }
    //...
private:
    HealthCalcFunc healthFunc;
};

HealthCalcFunc是个typedef,用来表现tr1::function的某个具现体,意味该具现体的行为像一般的函数指针。

现在来具体看看HealthCalcFunc是个什么样的typedef。int (const GameCharacter&)>代表的函数是“接受一个reference指向const GameCharacter,并返回int”。

这个tr1::function类型(即定义的HeathCalcFunc类型)产生的对象可以持有(保存)任何与此签名式兼容的可调用物。所谓兼容,意思是这个可调用物的参数可被隐式转换为

const GameCharacter&,而其返回类型可被隐式转换为int。

与前一个设计(其GameCharacter持有的是函数指针)比较,这个设计几乎相同。唯一不同的是如今GameCharacter持有一个tr1::function对象,相当于一个指向函数的泛化指针。

(二)tr1::function带来的改变 

这个改变很细小,几乎没有什么外显影响,除非客户在“指定健康计算函数”这件事上更需要更惊人的弹性:

short calcHealth(const GameCharacter&);//健康计算函数,注意其返回类型为non-int
struct HealthCalculator {//为计算健康而设计的函数对象
    int operator()(const GameCharacter&) const{/*...*/ }
};
class GameLevel {
public:
    //成员函数,用以计算健康;注意其non-int返回类型
    float health(const GameCharacter&) const;
    //...
};
class EvilBadGuy : public GameCharacter {//同前
    //...
};
//另一个人物类型,假设其构造函数与EvilBadGuy相同
class EyeCandyCharacter : public GameCharacter {
    //...
};
//人物1,使用某个函数计算健康指数
EvilBadGuy ebg1(calcHealth);
//人物2,使用某个函数对象计算健康指数
EyeCandyCharacter ecc1(HealthCalculator());
GameLevel currentLevel;
//...
//人物3,使用某个成员函数计算健康指数
EvilBadGuy ebg2(std::tr1::bind(&GameLevel::health),currentLevel,_1){}

对于上述代码,为计算ebg2的健康指数,应该使用GameLevel class的成员函数health。GameLevel::health宣称它自己接受一个参数(那是个reference,指向GameCharacter),但它实际上接受两个参数,因为它也获得一个隐式参数GameLevel,也就是this所指的那个。

然而,GameCharacter的健康计算函数只接受单一参数:GameCharacter(这个对象将被计算出健康指数)。若我们使用GameLevel::health作为ebg2的健康计算函数,我们必须以某种方式转换它,使它不再接受两个参数(一个GameCharacter和一个GameLevel),转而接受单一参数(一个GameCharacter)。

在这个例子中我们必然会想要使用currentLevel作为“ebg2的健康计算函数所需的那个GameLevel对象”,于是我们将currentLevel绑定为GameLevel对象,让它在“每次GameLevel::health被调用以计算ebg2的健康”时被使用。那正是tr1::bind的作为:它指出ebg2的健康计算函数应该总是以currentLevel作为GameLevel对象。

上述分析跳过了一大堆细节,像是为什么“_1”意味“当为ebg2调用GameLevel::health时以currentLevel作为GameLevel对象”。

这样的细节不难阐述,但它们会妨碍要讲的根本重点:

若以tr1::function替换函数指针,我们将因此允许客户在计算人物健康指数时使用任何兼容的可调用物。

四、古典的Strategy模式 

传统(典型)的Strategy做法会将健康计算函数做成一个分离的继承体系中的virtual成员函数。

设计结果看起来像这样:

上图表示GameCharacter是某个继承体系的根类,体系中的EvilBadGuy和EyeCandyCharacter都是derived class,HealthCalcFunc是另一个继承体系的根类,体系中的SlowHealthLoser和FastHealthLoser都是derived class,每一个GameCharacter对象都内含一个指针,指向一个来自HealthCalcFunc继承体系的对象。

下面是对应的代码骨干:

class GameCharacter;//前置声明
class HealthCalcFunc {
public:
    //...
    virtual int calc(const GameCharacter& gc) const{/*...*/ }
    //...
};
HealthCalcFunc defaultHealthCalc;
class GameCharacter {
public:
    explicit GameCharacter(HealthCalcFunc* phcf=&defaultHealthCalc)
        :pHealthCalc(phcf){}
    int healthValue() const
    {
        return pHealthCalc->calc(*this);
     }
    //...
private:
    HealthCalcFunc* pHealthCalc;
};

这个解法的吸引力在于,熟悉标准Strategy模式的人很容易辨认它,而且它还提供“将一个既有的健康算法纳入使用”的可能性——只要为HealthCalcFunc继承体系添加一个derived class即可。

五、总结 

本条款的根本忠告是,当你为解决问题而寻找某个设计方法时,不妨考虑virtual函数的替代方案。

(一)virtual函数的替代方案 

1.使用non-virtual interface(NVI)手法,那是Template Method设计模式的一种特殊形式。它以public non-virtual成员函数包裹较低访问性(private或protected)的virtual函数。

2.将virtual函数替换为“函数指针成员变量,这是Strategy设计模式的一种分解表现形式。

3.以tr1::function成员变量替换virtual函数,因而允许使用任何可调用物搭配一个兼容于需求的签名式。这也是Strategy设计模式的某种形式。

4.将继承体系内的virtual函数替换为另一个继承体系内的virtual函数。这是Strategy设计模式的传统实现手法。 

(二)注意 

1.virtual函数的替代方案包括NVI手法及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式。

2.将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法访问class的non-public成员。

3.tr1::function对象的行为就像一般函数指针。这样的对象可接纳“与之给定的目标签名式兼容”的所有可调用物。

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

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

相关文章

Linux x86_64平台同时编译x86_64和arm64两个架构的Qt应用程序出现XRes库无法找到

一 背景 在ubuntu x86_64平台上需要同时编译x86_64和arm64两个架构的Qt应用程序。在实践过程中&#xff0c;发现XRes库只能安装在其中一个平台。 二 根因 安装amd64版本的XRes库会删除arm64版本的库&#xff0c;反之亦然。 在安装amd64版本时&#xff0c;会删除arm64版本&a…

内部员工有没有在线帮助文档可以使用呢

当今企业中&#xff0c;内部员工的工作内容变得越来越复杂&#xff0c;需要不断学习新的知识和技能。在这种情况下&#xff0c;企业需要给员工提供一种便捷的在线帮助文档&#xff0c;使员工能够更加高效地完成工作任务。本文将介绍企业内部员工在线帮助文档的重要性以及如何建…

区块链如何助力价值互联网?这些专家有话说

6月13日&#xff0c;由开放原子开源基金会主办&#xff0c;XuperCore开源项目承办的2023开放原子全球开源峰会区块链分论坛在北京举办。来自区块链领域内的专家、行业代表汇聚一堂&#xff0c;分别以主题演讲、圆桌论坛等形式&#xff0c;阐述了全球区块链行业的新理念、新发展…

Opencv仿射函数getAffineTransform底层实现原理

推导 三角形ABC仿射成为三角形DEF的变换矩阵M 猜测矩阵M [ [a1,b1,c1], [a2,b2,c2] ] 仿射变换的数学联系 对于A点和D点 AX*a1AY*b1c1DX AX*a2AY*b2c1DY 对于B点和E点 BX*a1BY*b1c1EX BX*a2BY*b2c2EY 对于C点和F点 CX*a1CY*b1c1FX CX*a2CY*b2c2FY 求解 对以上数…

seqkit 两种拆分方法的比较

seqkit拆分fastq&#xff0c;fasta等文件有两种方式&#xff1a;split和split2&#xff0c; 二者的逻辑并不一样。 split 是将原文件拆分&#xff0c;写满第一个文件&#xff0c;再写第二个文件 split2是将原文件的内容&#xff0c;挨个写到各个拆分文件里面去&#xff1f; 比…

拓尔微技术干货 | get 5个知识点,了解细分驱动技术

本期我们对步进电机驱动原理、五线的步进电机和四线的步进电机、2-2相励磁和1-2相励磁、步进电机驱动的优化方案、拓尔微TMI8150B细分驱动技术的原理进行详细的介绍&#xff0c;共分为5个知识点&#xff0c;全是技术干货&#xff0c;赶紧get下来~ ✔get 1&#xff1a;步进电机…

datax安装部署使用 windows

Datax在win10中的安装_windows安装datax_JMzz的博客-CSDN博客 DataX/userGuid.md at master alibaba/DataX GitHub 环境准备&#xff1a; 1.JDK(1.8以上&#xff0c;推荐1.8) 2.①Python(推荐Python2.7.X) ②Python(Python3.X.X的可以下载下面的安装包替换) python3.0需…

Docker安装——CentOS7.6(详细版)

ps:docker官网 在 CentOS 上安装 Docker 引擎 |官方文档 &#xff08;&#xff09; 一、确定版本&#xff08;必须是7以上版本&#xff09; cat /etc/redhat-release 二、卸载旧版本&#xff08;或者之前装过&#xff0c;没有安装过就不用管了&#xff09; &#xff08;root用…

OpenCV 项目开发实战--实现填充图像中的孔( Python / C++ ) 代码实现

文末附分别基于C++和python实现的相关测试代码下载链接 图 1. 左:原始的图像。中心:阈值和倒置。右:孔被填充。 在本教程中,我们将学习如何填充二值图像中的孔洞。考虑图 1 中左侧的图像。假设我们想要找到一个将硬币与背景分开的二进制掩码,如右图所示。在本教程中,包含…

2.0C++继承

C继承概述 C 中的继承是指一个类可以从另一个类中继承属性和方法&#xff0c;这个被继承的类称为基类或父类&#xff0c;继承它的类称为派生类或子类。 C三种继承 1、公有继承 public 在公有继承中&#xff0c;基类的公有成员和保护成员都可以被派生类访问&#xff0c;而基…

【ROS】ROS2导航Nav2:简介、安装、测试效果、错误处理

1、简介 在ROS2中自动导航使用Nav2来实现。 Nav2 使用几个独立的模块化服务&#xff0c;通过 ROS 2接口&#xff08;例如动作服务器或服务&#xff09;与行为树 (BT) 通信。 Nav2 输入包括&#xff1a;TF转换、一个地图源、一个行为树 (BT) XML 文件和相关的传感器数据源; Nav…

【数字图像处理】2.几何变换

目录 2.1 仿射变换&#xff08;二维&#xff09; 2.2 投影变换&#xff08;三维&#xff09; 2.3 极坐标变换 2.3.1 将笛卡尔坐标转化为极坐标 2.3.2 将极坐标转换为笛卡尔坐标 2.3.3 利用极坐标变换对图像进行变换 几何变换&#xff1a;放大、缩小、旋转等&#xff0c;改…

[游戏开发]Unity中随机位置_在圆/椭圆/三角形/多边形/内随机一个点

[ 目录 ] 0. 前言1. 矩形内随机2. 圆形内随机3. 三角形内随机4. 多边形内随机&#xff08;1&#xff09;多边形分割为三角形&#xff08;2&#xff09;三角形面积计算&#xff08;3&#xff09;数据缓存&#xff08;4&#xff09;按权重随机&#xff08;5&#xff09;实现随机 …

理论【8】TCPUDP彻底搞懂了吗?

1 概述 对于TCP协议和UDP协议&#xff0c;大家应该都有所耳闻。TCP协议和UDP协议都工作在传输层&#xff0c;他们的目标都是在应用之间传输数据。我们常用的网络通信&#xff0c;比如浏览网页、查看邮件、电话通信等&#xff0c;都是通过这两种协议来进行数据传输的。 2 TCP…

面试---如何发现Redis热点Key,有哪些解决方案?

目录 热点问题概述热点问题的危害常见解决方案阿里云数据库解热点之道热点key的处理两种方案对比总结 热点问题概述 产生原因 热点问题产生的原因大致有以下两种&#xff1a; 用户消费的数据远大于生产的数据&#xff08;热卖商品、热点新闻、热点评论、明星直播&#xff09;…

【AI绘画】本地部署Stable Diffusion Web UI

近几年AI发展迅猛&#xff0c;今年更是大爆发&#xff0c;除了爆火的大模型ChatGPT以外&#xff0c;AI绘画也有很大的进步&#xff0c;目前&#xff0c;Stable Diffusion图像生成效果相当惊人&#xff0c;可以生成逼真的人像、风景、物品图片&#xff0c;还可以将图片转换为不同…

提示学习soft prompt浅尝,启发了p-tuing

一、前言 在高质量标注数据稀缺的工业界来说&#xff0c;少样本学习或者零样本学习的方法特别受欢迎&#xff0c;后面出现过一些少样本和零样本的方法&#xff0c;例如对比学习和prompt等&#xff0c;主流prompt的工作分为离散型和连续型模板。离散型主要还是插入bert特殊的tok…

分享AI绘画的方法

曾经&#xff0c;在一个神奇的编程国度里&#xff0c;住着一个名叫小花的程序员。小花喜欢创造和探索新奇的技术&#xff0c;她有一个惊人的能力&#xff1a;她能够根据文字生成相应的图片。这项技术让她成为了这个国度里的传奇人物。人们纷纷向她寻求帮助&#xff0c;希望能够…

flutter:网络请求、json数据转为Model

参考 老孟 flutter&#xff1a; 网络请求-dio http http 是一个可组合&#xff0c;基于Future的库&#xff0c;用于HTTP请求。该软件包包含高级功能和类&#xff0c;可轻松使用HTTP资源。它是多平台的&#xff0c;并且支持移动设备&#xff0c;台式机和浏览器。此软件包为官…

STM32F1x固件库函数学习笔记(一)

文章目录 一、基础知识1、什么是STM322、STM32诞生背景3、STM32分类4、STM32F1X系列命名规则5、STM32F103C8T6最小系统 二、STM32固件库1、初始固件库&#xff08;1&#xff09;51单片机的寄存器&#xff08;2&#xff09;STC8A通过库函数方式实现LED闪烁&#xff08;3&#xf…