考虑virtual函数以外的其他选择——条款35

2023-11-11

        假设你正在写一个视频游戏软件,你打算为游戏内的人物设计一个继承体系。你的游戏术语暴力砍杀类型,剧中人物被伤害或因其他因素而降低健康状态的情况并不罕见。你因此决定提供一个成员函数healthValue,它会返回一个整数,表示人物的健康程度。由于不同的人物可能以不同的方式计算他们的健康指数,将healthValue声明为virtual似乎是再明白不过的做法:

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

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

       这的确是再明白不过的设计,但是从某个角度说却反而成了它的弱点。由于这个设计如此明显,你可能因此没有认真考虑其他代替方案。为了帮助你跳脱面向对象设计路上的常轨,让我们考虑其他一些解法。

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

        我们将从一个有趣的思想流派开始,这个流派主张virtual函数应该几乎总是private。这个流派的拥护者建议,较好的设计是保留healthValue为public成员函数,但让它成为non-virtual,并调用一个private virtual函数(例如doHealthValue)进行实际工作:

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

        在这段(以及本条款其余的)代码中,我直接在class定义式内呈现成员函数本体,一如条款30所言,那也就让它们全都暗自成了inline,但其实我以这种方式呈现代码只是为了让你比较容易阅读。我所描述的设计与inlining其实没有关联,所以请不要以为成员函数在这里被定义于classes内有特殊用意。不,它没有。

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

籍由Function Pointers实现Strategy模式

        NVI手法对public virtual函数而言是一个有趣的替代方案,但从某种设计角度观之,它只比窗饰花样更强一些而已。毕竟我们还是使用virtual函数来计算每个人物的健康指数。另一个更戏剧性的设计主张“人物健康指数的计算与人物类型无关”,这样的计算完全不需要“人物”这个成分,例如我们可能会要求每个人物的构造函数接受一个指针,指向一个健康计算函数,而我们可以调用该函数进行实际计算:

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

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

  • 同一人物类型之不同实体可以有不同的健康计算函数。例如:
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);              // 不同的健康计算方式
  • 某已知人物之健康指数计算函数可在运行期变更。例如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函数,其优点(像是“每个对象可各自拥有自己的健康计算函数”和“可在运行期改变计算函数”)是否足以弥补缺点(例如可能必须降低GameCharacter封装性),是你必须根据每个设计情况的不同而抉择的。

籍由tr1::function完成Strategy模式

        一旦习惯了templates以及它们对隐式接口(见条款41)的使用,基于函数指针的做法看起来便过分苛刻而死板了。为什么要求“健康指数之计算”必须是个函数,而不能是某种“像函数的东西”(例如函数对象)呢?如果一定得是函数,为什么不能够是个成员函数?为什么一定得返回int而不是任何可被转换为int的类型呢?

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

class GameCharacter;                                 // 如前
int defaultHealthCalc(const GameCharacter& gc);      // 如前
class GameCharacter {
public:
	// HealthCalcFunc可以是任何“可调用物”(callable entity),可被调用并接受
	// 任何兼容于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:

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

        那个签名代表的函数是“接受一个reference指向const GameCharacter,并返回int”。这个tr1::function类型(也就是我们所定义的HealthCalcFunc类型)产生的对象可以持有(保存)任何与此签名式兼容的可调用物(callable entity)。所谓兼容,意思是这个可调用物的参数可被隐式转换为const GameCharacter&,而其返回类型可被隐式转换为int。

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

古典的Strategy模式

        如果你对设计模式(design patterns)比对C++的酷劲更有兴趣,我告诉你,传统(典型)的Strategy做法会将健康计算函数做成一个分离的继承体系中的virtual成员函数。设计结果看起来像这样:

        如果你并未精通UML符号,别担心,这图只是告诉你GameCharacter是个继承体系的根类,体系中EvilBadGuy和EyeCandyCharacter都是derived classes:HealthCalcFunc是另一个继承体系的根类,体系中的SlowHealthLoser和FastHealthLoser 都是derived classes,每一个GameCharacter对象都内含一个指针,指向一个来自HealthCalcFunc继承体系的对象。

        下面是对应的代码骨干:

class GameCharacter;                // 前置声明(forward declaration)
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函数的替代方案。下面快速重点覆写我们验证过的几个替代方案:

  • 使用non-virtual interface(NVI)手法,那是Template Method设计模式的一种特殊形式。它以public non-virtual成员函数包裹较低访问性(private或protected)的virtual函数。
  • 将virtual函数替换为“函数指针成员变量”,这是Strategy设计模式的一种分解表现形式。
  • 以tr1::function成员变量替换virtual函数,因而允许使用任何可调用物(callable entity)搭配一个兼容于需求的签名式。这也是Strategy设计模式的某种形式。
  • 将继承体系内的virtual函数替换为另一个继承体系的virtual函数。这是Strategy设计模式的传统实现手法。

        以上并未彻底而详尽地列出virtual函数的所有替换方案,但应该足够让你知道的确有不少替换方案。此外,它们各有其相对的优点和缺点,你应该把它们全部列入考虑。

请记住

  • virtual函数的替代方案包括NVI手法及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式。
  • 将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无访问class的non-public成员。
  • tr1::function对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式(target signature)兼容”的所有可调用物(callable entities)。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

考虑virtual函数以外的其他选择——条款35 的相关文章

  • matlab获取矩阵的行数与列数

    matlab里面与其他高级语言里面获取数据的长度length方法不一样 matlab里面通过size 矩阵变量 返回一个 行数m 列数n 比如一个m n的矩阵A 通过size A 可以得到 m n 通过size A 1 可以得到行数m 通过

随机推荐

  • 关于如何使用neo4j-admin工具批量导入已处理好的csv数据(neo4j 社区版 5.5)

    数据格式有两种 一个是节点 一个是关系 节点类型数据头格式 xxx ID name LABEL 关系类型数据头格式 START ID END ID TYPE 这里不多赘述关于csv数据处理的问题 可以通过搜索找相关资料 本文主要解决的问题是
  • LSTM原理图解

    在解释LSTM原理前先来理解一下RNN的原理 RNN基本原理 原理简介 当我们处理与事件发生的时间轴有关系的问题时 比如自然语言处理 文本处理 文字的上下文是有一定的关联性的 时间序列数据 如连续几天的天气状况 当日的天气情况与过去的几天有
  • sqli-labs(39关-53关)

    目录 第三十九关 第四十关 第四十一关 第四十二关 第四十三关 第四十四关 第四十五关 第四十六关 第四十七关 第四十八关 第四十九关 第五十关 第五十一关 第五十二关 第五十三关 第三十九关 id 1 and 1 1 id 1 and 1
  • 图像处理-双边滤波原理

    双边滤波 Bilateral filter 是一种可以去噪保边的滤波器 之所以可以达到此效果 是因为滤波器是由两个函数构成 一个函数是由几何空间距离决定滤波器系数 另一个由像素差值决定滤波器系数 原理示意图如下 双边滤波器中 输出像素的值依
  • Midjourney如何集成到自己(个人/企业)的平台(二)

    前面一篇写了需要准备东西 如何注册discord平台账号 如何登录discord创建个人服务器把Midjourney机器人授权添加到个人服务器中 并且开通订阅 这篇文章主要讲如何自定义机器人 设置自定义机器人 并授权添加到个人服务器中 1
  • 【Arthas】Arthas mc内存动态编译原理

    1 概述 转载 Arthas mc内存动态编译原理 2 开篇 Arthas支持通过mc命令进行java文件的内存动态编译 mc Memory Compiler 内存编译器 编译 java文件生成 class 从JDK1 6开始引入了Java
  • 手握6项特许经营权,慧居科技如何展现“光与热”?

    作为国内三北地区第二大跨省供热服务供应商 慧居科技在7月10日即将港股上市 尽管目前受经济影响 港股市场处在低迷状态 但供热行业作为公用事业板块属刚性需求 由于受经济周期影响小 经营业绩稳定 反而成为市场的优质板块 吸引了不少的资本关注 7
  • Mac 电脑鼠标和触摸板滚动方向不一致的问题【已解决】

    当我们使用鼠标连接到 MacBook 时 会发现无论怎么设置 鼠标和触摸板的滚动方向都是相反的 导致不能同时使用鼠标和触摸板 解决方法 我安装了下面的程序 它只允许您反转鼠标的滚动行为 Scroll Reverser for Mac OS
  • 【人脸生成】HiSD-通过层级风格解耦实现图到图的迁移

    Image to image Translation via Hierarchical Style Disentanglement 厦大 西交 腾讯 清晰易读 用公布的模型在自有数据上实测不及预期 但仍是值得尝试的方法 这是我看的第一篇人脸
  • SQL基础常用语句:DDL、 DML、DQL

    下面跟我一起来学习SQL基础知识 一 SQL基础与DDL 1 1 SQL的概述 SQL全称 Structured Query Language 结构化查询语言 用于访问和处理数据库的标准的计算机语言 SQL语言1974年由Boyce和Cha
  • bitset优化例题

    1 bitset 优化背包 https loj ac p 515 题意 给 n 个 lt n 的数 每个数有取值范围 a i b i 令 x 为 n 个数的平方和 求能构成的 x 的个数 样例 5 1 2 2 3 3 4 4 5 5 6 2
  • js小程序ios日期解析失败NAN兼容

    小程序中ios使用 new Date 的时候 如果有 分隔符 将会解析失败 如果日期过短也会解析失败 比如只有 2022 08 年月这样也解析不出来 下面工具能解决上述问题 但是在手动创建字符串时间 建议使用 2022 08 01 斜杠等方
  • 第一次在linux服务器上部署项目,遇到的问题总结(包括mysql安装,jar打开遇到的问题)

    第一步 写代码 这一步没啥好说的 将代码写完 再考虑部署的事情 第二步 linux中安装mysql linux中安装mysql比在windows中安装mysql要难的多 其中遇到的问题是 一直安装不成功 老是会缺少 systemctl st
  • 解决Tensorflow读取MNIST数据集时网络超时问题

    最近在学习TensorFlow 比较烦人的是使用tensorflow examples tutorials mnist input data读取数据 from tensorflow examples tutorials mnist impo
  • cmake:file

    文件操作命令 这个命令专用于需要访问文件系统的文件和路径操作 对于其他仅处理语法方面的路径操作 请查看cmake path 命令 概要 Reading file READ
  • 南昊网上阅卷系统服务器地址,南昊网上阅卷系统全攻略

    扫描网上阅卷系统的分类 南昊扫描客观题阅卷系统 南昊扫描单机阅卷系统 南昊扫描网上阅卷系统 校园版 南昊扫描网上阅卷系统 中考版 南昊扫描网上阅卷系统 行业考试版 南昊扫描海军民主评议系统 南昊扫描教学质量评测系统 南昊扫描行风评议系统 网
  • hexo部署码云

    在本地搭建好博客后我们需要把博客部署在服务器上 这里没有购买服务器的情况下 就暂时部署在码云或者github上也是可以滴 如果之前部署过github的童鞋应该很快就上手了 都差不多的 这里就没有配置本地公钥了 直接使用是http链接方式 g
  • 利用k8s部署nginx

    这只是一个简单的demo测试 记录下第一次部署应用哈哈哈哈 运行指令创建有四个副本的nginx 这点和docker挺像的 root master kubectl run nginx image nginx latest replicas 4
  • Java实现从FTP获取文件下载到本地,并读取文件中的内容的成功案例

    package com aof web servlet import java io BufferedReader import java io File import java io FileInputStream import java
  • 考虑virtual函数以外的其他选择——条款35

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