条款35:考虑virtual函数以外的其它选择

1.前言

假设我们在写一个3D游戏软件,打算为游戏内的人物设计一个继承体系。游戏内容属于暴力砍杀类型,游戏中的角色被伤害或其它因素导致健康状态下降的情况是一个常见属性。因此设计一个成员函数healthValue,它会返回一个整数,表示人物的健康程度。

2.具体分析

由于不同的任务可能以不同的方式计算它们的健康指数,将healthValue声明为virtual似乎是最直白的做法:

class GameCharacter{public:virtual int healthValue() const;//返回人物的健康指数....
};

在这里,healthyValue并未被声明未pure virtual,这暗示我们将会有个计算健康指数的缺省算法。这的确是个很直白的设计,但是从某个角度说却反而成为它的弱点了。由于这个设计太过于明显,导致我们可能都没有认真考虑过其它的替代方案,这里我们考虑用其它方案来替代。

由Non-Virtual Interface手法实现Template Method模式

首先我们先从一个思想流派说起,该流派认为virtual函数应该几乎总是private。该流派认为:较好的设计是保留healthValue为public成员函数,但让它成为non-virtual,并调用一个private virtual函数(比如doHealthValue)进行实际工作:

class GameCharacter{public:int healthValue() const{...int retVal=doHealthValue();...return retVal;}...private:virtual int doHealthValue() const//derived classes可重新定义该函数{...//缺省算法,计算健康指数}
};

在这段代码中,我直接在class定义式内呈现函数本体。

这一基本设计,也就是”令客户通过public non-virtual成员函数间接调用private virtual函数“,称该方法为NVI(non-virtual interface)手法。它是所谓template Method设计模式的一个独特表现形式,我把这个non-virtual函数(healthValue)称为virtual函数的外覆器

该方法的一个优点是在上述代码注释”做一些事前工作“和”做一些事后工作“之中。那些注释用来告诉你当时的代码保证在”virtual函数进行真正工作之前和之后被调用“。这意味着外覆器(wrapper)确保得以在一个virtual函数被调用之前设定好适当的场景,并在调用结束之后清理场景。”事前工作“可以包括锁定互斥器(locking a mutex),制造运转日志记录项(log entry),验证class约束条件,验证函数先决条件。”事后工作“可以包括互斥器解锁锁定(unlocking a mutex),验证函数的事后条件,再次验证class约束条件等等,倘如让客户直接调用virtual函数,就没有啥办法做这些事情了。

NVI方案涉及在derived classes内重新定义private virtual函数。”重新定义virtual函数“表示某些事如何被完成,”调用virtual函数“表示其什么时候被完成。这些事情都是各自独立互不相关的。NVI方案允许derived classes重新定义virtual函数,但base class保留”函数何时被调用“的权利。

在NVI方案下其实没有必要让vitual函数一定得是private,某些class继承体系要求derived class在virtual函数的实现内必须调用base class的对应函数,而为了让这样的调用合法,virtual函数必须是protected,不能是private。有时候virtual函数甚至一定是public,这种情况下,就不能实施NVI方案了。

籍由Function Pointers实现Strategy模式

NVI方案对public virtual函数而言是一个有趣的替代方案,但从某种设计角度来说,它还没脱离virtual的本质。毕竟我们还是使用virtual函数来计算每个人物的健康指数。

这里有另外一种设计方案,该方案主张”人物健康指数的计算与人物类型无关“,这样的计算完全不需要“人物”这个成分。例如我们可能会要求每个人物的构造函数接收一个指针,指向一个健康计算函数,而我们可以调用该函数进行实际计算:

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

这种做法是常见的Strategy设计模式的简单应用,它与GameCharacter继承体系内的virtual函数相比,具有以下这些特点:

同一人物类型的不同实体可以有不同的健康计算函数,比如:

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

若已知人物的健康计算函数可以在运行期间变更,例如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函数为friends,或是为其实现的某一部分提供public访问函数。运用函数指针替换virtual函数

籍由trl::function完成Strategy模式

一旦习惯了templates以及它们对隐式接口的使用,基于函数指针的做法看起来就显得过于死板了。这里有个问题,为什么一定得是函数,为什么不能够是个成员函数,为什么一定得返回Int而不是任何可被转换为int得类型呢?

假设我们不再使用函数指针(如前面得healthFunc),而是改用一个类型为trl::function的对象,这些约束就全部消失不见了。见以下例子:

trl::function:
class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter{//HealthCalcFunc可以是任何“可调用物”,可被调用并接受//任何兼容于GameCharacter之物,返回任何兼容于int的东西typedef std::trl::function<int (const GameCharacter&)> HealthCalcFunc;explict GameCharacter(HealthCalcFunc hcf=defaultHealthCalc):healthFunc(hcf){ }int healthValue() const{return healthFunc(*this);}...private:HealthCalcFunc healthFunc;};

如你所见,HealthCalcFunc是个typedef,用来表现trl::function的某个具现化,意味着该具现化的行为像一个一般的函数指针。现在我们靠近一点瞧瞧HealthCalcFunc是个什么样的typedef:

HealthCalcFunc是个什么样的typedef:

std::trl::function<int (const GameCharacter&)>

这里我把trl::function具现体的目标签名以不同的颜色强调出来。那个签名代表的函数是“接受一个reference指向const GameCharacter,并返回int”。这个trl::function类型(即HealthCalcFunc)产生的对象可以持有任何与此签名式兼容的可调用物。所谓兼容,即这个可调用物的参数可以被隐式转换为const GameCharacter&,而其返回类型可被隐式转换为int。

和前一个设计比较,这个设计几乎相同。唯一不同的是如今GameCharacter持有一个trl::function对象,相当于一个指向函数的泛化指针。见下例子:

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

就我个人而言,当我发现trl::function允许我们做的事时非常惊讶。

首先我要表明,为计算ebg2的健康指数,应该使用GameLevel class的成员函数health,GameLevel::health宣称它自己接受一个参数(那是reference 指向GameCharacter),但实际上它接受两个参数,因为它也获得一个隐式参数GameLevel,也就是this所指的那个。然而GameCharacter的健康计算函数只接受单一参数:GameCharacter,如果我们使用GameLevel::health作为ebg2的健康计算函数,我们必须以某种方式转换它,使它不再接受两个参数,转而接受单一参数(一个GameCharacter)。

3.总结

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

1.使用non-virtual interface方案,那是Template method设计模式的一种特俗形式。它以public non-virtual成员函数包裹较低访问性的virtual函数

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

3.以trl::function成员变量替换virtual函数,因而允许使用任何可调用物搭配一个兼容于需求的签名式

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

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

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

相关文章

2024年天津体育学院专升本专业考试考生入场及考前须知

天津体育学院2024年高职升本科招生专业考试考生考前须知 一、考生入场及考试要求 1.考生于1月6日笔试考试当天&#xff0c;根据考试时间提前30分钟到达天津体育学院新校区东门&#xff0c;凭专业考试准考证、有效身份证原件&#xff0c;经查验合格后方可允许进入学校。 2.笔…

Linux 中 EXPORT_SYMBOL宏详解

Linux 中 EXPORT_SYMBOL宏详解 大家好&#xff0c;我是免费搭建查券返利机器人赚佣金就用微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01;在今天的文章中&#xff0c;我们将深入研究C/C编程中一个关键的宏——EXPORT_SYMBOL&…

PLC分段传送带控制示例

一、为了节约用电&#xff0c;将长长的传送带进行分段&#xff0c;哪断上有物品&#xff0c;哪断才运行 二、每一断末尾都有传感器&#xff0c;传感器能感受到物体有没有到传送带的末尾 三、这个传感器是接近开关 四、控制流程 五、IO地址分配 六、按下启动按钮后&#xff0c;…

问题 C: 活动选择

题目描述 学校在最近几天有n个活动&#xff0c;这些活动都需要使用学校的大礼堂&#xff0c;在同一时间&#xff0c;礼堂只能被一个活动使。由于有些活动时间上有冲突&#xff0c;学校办公室人员只好让一些活动放弃使用礼堂而使用其他教室。    现在给出n个活动使用礼堂的起…

Apache网页优化

本章主要介绍如何对Apache网页进行优化 Apache 网页压缩Apache 网页缓存Apache 隐藏版本信息Apache 网页防盗链 目录 1、网页压缩与缓存 1.1、网页压缩 &#xff08;1&#xff09;gzip介绍 &#xff08;2&#xff09;HTTP压缩的过程 &#xff08;3&#xff09;Apache的…

免费的GPT4来了,你还不知道吗?

程序员的公众号&#xff1a;源1024&#xff0c;获取更多资料&#xff0c;无加密无套路&#xff01; 最近整理了一波电子书籍资料&#xff0c;包含《Effective Java中文版 第2版》《深入JAVA虚拟机》&#xff0c;《重构改善既有代码设计》&#xff0c;《MySQL高性能-第3版》&…

在Linux写自己的第一个程序“hello Linux”

01.nano指令 我们在Windows中有很多的编译环境&#xff0c;大家应该都很熟悉&#xff0c;但是在Linux中&#xff0c;我们怎么写代码呢&#xff1f; 这里&#xff0c;我介绍一个非常简单的指令->nano 这个指令就类似于我们Windows中的记事本&#xff0c;使用方法也很简单 …

网络连接 UDP2,UDP Connect, bind, send, recieve认知, -入门8

LWIP编程接口有RAW, NETCONN, SOCKET 2.UDP函数的理解 #define UDP_SERVER_PORT 8000 //PC side #define UDP_CLIENT_PORT 1234 // ctrl board side //PC IP address #define DEST_IP_ADDR0 192 #define DEST_IP_ADDR1 168 #define DEST_IP_ADDR2 3 #define DEST_IP_ADDR3 11…

SRS服务器RTMP2WebRTC外网拉流配置

将前端服务开放到外网&#xff0c;并且在外网使用WebRTC拉流&#xff0c;要求全部使用同一端口。 外网地址&#xff1a;10.172.59.42&#xff08;域名&#xff1a;test.com&#xff09;外网端口为33333内网SRS服务器地址&#xff1a;192.168.5.177&#xff0c;内网端口为10090…

算法训练day60|单调栈part0

参考&#xff1a;代码随想录 84.柱状图中最大的矩形 要求当前柱形的左右两边第一个比他小的位置 对于高度为5的柱子&#xff08;index为2&#xff09; mid 他的左边第一个比他小的柱子为1&#xff0c;index为1 left 他的右边第一个比他小的柱子高度为2&#xff0c;index为4…

待做事项提醒 项目

GitHub - kesin/taskover: Taskover is a simple planlist tools to manage your task easily. app-version - APP 版本管理系统 源码地址&#xff1a;https://github.com/xtTech/app-version/ 考试答题&#xff1a;https://gitee.com/beautiful-life/exam 追风考试系统 vu…

【sed学习】sed -i和sed -i -e有什么区别

前言&#xff1a; 项目脚本有句sed -i $aPRODUCT_PACKAGES rtk_mdnsd device.mk sed -i -e $a/vendor/bin/ipv4lld u:object_r:ipv4lld_exec:s0 sepolicy/file_contexts不太清楚sed -i和sed -i -e有什么区别&#xff0c;学习一下 sed -i 和 sed -i -e 是用于编辑文件中匹配模式…

GB∕T 33171-2016 城市交通运行状况评价规范

免登陆免积分下载地址 标准号&#xff1a;GB/T 33171-2016 中文标准名称&#xff1a;城市交通运行状况评价规范 英文标准名称&#xff1a;Specification for urban traffic performance evaluation 中国标准分类号&#xff08;CCS&#xff09;R85 国际标准分类号&#xff08;…

提前终止 Lambda forEach 的两种方法

在Java中&#xff0c;Lambda表达式提供了一种简便的方式来对集合进行迭代处理。然而&#xff0c;有时我们可能希望在特定条件下提前终止forEach的执行。这篇博客将介绍两种实现这一目标的方法。 方法一&#xff1a;使用异常 我们可以通过在Lambda表达式中抛出自定义异常的方式…

软件测试|SQL中的null值,该如何理解?

深入理解SQL中的Null值&#xff1a;处理缺失数据的重要概念 简介 Null值在SQL中是用于表示缺失或未知数据的特殊值。本文将深入探讨Null值的概念、处理方法和注意事项&#xff0c;以帮助读者更好地理解和处理SQL中的缺失数据。 在SQL数据库中&#xff0c;Null值是一种特殊的…

gitlab高级功能之Kubernetes Agent介绍

文章目录 1. 前置条件2. 简介3. GitLab Kubernetes Agent 的部署3.1 启用 Agent 服务端3.2 创建 Agent 配置和清单仓库 4. 安装agent4.1 连接k8s集群4.2 在集群中部署4.3 修改资源清淡&#xff0c;调整pod的副本数 5. 思考 1. 前置条件 gitlab 14.5 专业版k8s集群helm客户端工…

xadmin-plus

python之Xadmin-plus是什么&#xff1f; xadmin-plus: xadmin的django3.2版本支持。 Xadmin是一个非常优秀的Django Admin插件&#xff0c;可惜的是已经停止更新。Xadmin-plus对其进行了升级兼容。支持python3.10、Django3.2。 特性 Django Admin直接替换基于Twitter Boots…

uniapp存储讲解

在 Uni-app 中&#xff0c;你可以使用本地缓存来存储应用程序的数据。Uni-app 提供了两种本地缓存方式&#xff1a;uni.setStorageSync 和 uni.setStorage。其中&#xff0c;uni.setStorageSync 同步方式存储数据&#xff0c;适合小数据量的存储&#xff1b;而 uni.setStorage …

uniapp 跨页面传参的几种方式

当我们在开发Uni-app应用时&#xff0c;经常会遇到需要在不同页面之间传递参数的情况。为了实现跨页面传参&#xff0c;Uni-app提供了以下几种方式&#xff1a; URL传参&#xff1a;这是一种简单且常用的方式。在跳转页面时&#xff0c;可以通过在URL中添加参数来传递数据。目…

Docker 发布自定义镜像到公共仓库

Docker 发布自定义镜像到公共仓库 引言 Docker 是一种轻量级、便携式的容器化技术&#xff0c;可以使应用程序在不同环境中更加可移植。在本文中&#xff0c;我们将学习如何使用 Docker 从公共仓库拉取 Nginx 镜像&#xff0c;定制该镜像&#xff0c;添加自定义配置文件&…