考虑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 的相关文章

  • Effective C++

    条款01 视C 为一个语言联邦 将 视为一个由相关语言组成的联邦而非单一语言 条款02 尽量以const enum inline替换 define define处理与预处理阶段 而非编译阶段 因此此条款也可称为 宁可以编译器替换预处理器比较
  • 23种设计模式之策略模式

    文章目录 概览 策略模式的优缺点 策略模式的应用场景 策略模式的结构与实现 模式的结构 模式的实现 策略模式的扩展 总结 概览 策略模式定义了一系列算法 并将每个算法封装起来 使他们可以相互替换 且算法的变化不会影响到使用算法的客户 需要设
  • 编写new和delete时需固守常规——条款51

    条款50已解释什么时候你会想要写个自己的operator new和operator delete 但并没有解释当你那么做时必须遵守什么规则 这些规则不难奉行 但其中一些并不直观 所以知道它们究竟是些什么很重要 让我们从operator ne
  • 桥接模式与策略模式的区别

    文章转载自 http www blogjava net wangle archive 2007 04 25 113545 html 桥接 Bridge 模式是结构型模式的一种 而策略 strategy 模式则属于行为模式 以下是它们的UML
  • 通过Comparator接口理解策略模式

    在Java中 如果我们想要比较两个对象 一般会让这个对象实现Comparable接口 比如 在下面的代码中 Person类有两个属性 分别是height和weight 为了比较大小 我们这里重写了compareTo方法 通过height属性
  • 设计模式-策略模式

    策略模式是一种行为型设计模式 其主要目的是允许在运行时选择算法的行为 在Java中 我们可以使用策略模式来根据不同的条件动态地选择不同的算法 下面是一个示例代码 展示了如何在Controller中确定是什么策略 以及如何调用相应的Servi
  • 透彻了解inlining的里里外外——条款30

    Inline函数 多棒的点子 它们看起来像函数 动作像函数 比宏好得多 见条款2 可以调用它们又不需要蒙受函数调用所招致的额外开销 你还能要求更多吗 你实际获得的比想到的还多 因为 免除函数调用成本 只是故事的一部分而已 编译器最优化机制通
  • 行为型设计模式之策略模式【设计模式系列】

    系列文章目录 C 技能系列 Linux通信架构系列 C 高性能优化编程系列 深入理解软件架构设计系列 高级C 并发线程编程 设计模式系列 期待你的关注哦 现在的一切都是为将来的梦想编织翅膀 让梦想在现实中展翅高飞 Now everythin
  • @Async 异步调用

    策略模式 一 Async 基础 基础使用示例 二 Async 与线程池 实现AsyncConfigurer 替换默认线程池 指定 Async 使用的线程池 一 Async 基础 在编写接口时大多数情况下都是通过同步的方式来实现交互处理 在特
  • 策略+工厂+反射记录一次switch代码简化过程

    遇到的问题 一张记录表 记录了10个业务的字段 一个入参type说明了要修改哪个字段 最初是通过switch type case 来做的 但是涉及这样子的判断多了 每次都要不断的switch 并且case里面不同方法有不同的处理 一个公共的
  • 模式分类与“组件协作模式”

    1 GOF 23 模式分类 从目的来看 创建型 Creational 模式 将对象的部分创建工作延迟到子类或者其他对象 从而应对需求变化为对象创建时具体类型实现引来的冲击 结构型 Structural 模式 通过类继承或者对象组合获得更灵活
  • cpp: Strategy Pattern

    Gold h 此文件包含 Gold 类 策略模式 Strategy Pattern C 14 2023年5月1日 涂聚文 Geovin Du Visual Studio 2022 edit pragma once ifndef GOLD H
  • 设计模式--策略模式

    文章目录 策略 Strategy 模式 策略模式的收银软件 策略模式的特点 使用场景 优缺点 策略模式和工厂模式的结合 策略 Strategy 模式 本质 分离算法 选择实现 策略模式 针对一组算法 将每一个算法封装到具有共同接口的独立的类
  • 条款13: 以对象管理资源

    结论 为防止资源泄漏 请使用RAII对象 它们在构造函数中获得资源并在析构函数中释放资源 两个常被使用的RAII classes分别是tr1 share ptr和auto ptr 前者通常是较佳选择 因为其copy行为比较直观 若选择aut
  • Effective C++改善程序与设计的55个具体做法笔记

    Scott Meyers大师Effective三部曲 Effective C More Effective C Effective STL 这三本书出版已很多年 后来又出版了Effective Modern C More Effective
  • 简单工厂模式和策略模式的比较

    代码结构图的区别 首先来看一下简单工厂模式 再看一下策略模式 看完他们的结构图 是不是有种很相似的感觉 唯一不同的就是 简单工厂类 和 Context类 接下来再看一下代码上有什么区别 简单工厂类和Context类中代码的区别 简单工厂类
  • 复制对象时勿忘其每一个成分——条款12

    设计良好之面向对象系统 OO systems 会将对象的内部封装起来 只留两个函数负责对象拷贝 复制 那便是带着适切名称的copy构造函数和copy assignment操作符 我称它们为copying函数 条款5观察到编译器会在必要的时候
  • 策略模式-

    定义 定义一系列的算法 把它们一个个封装起来 目的就是将算法的使用与算法的实现分离开来 从而算法的变化不会影响到使用算法的用户 适用场景 1 假如系统中有很多类 而他们的区别仅仅在于他们的行为不同 2 一个系统需要动态地在几种算法中选择一种
  • 策略模式在数据接收和发送场景的应用

    其他系列文章导航 Java基础合集 数据结构与算法合集 设计模式合集 多线程合集 分布式合集 ES合集 文章目录 其他系列文章导航 文章目录 前言 一 策略模式改进 1 1 策略模式的定义 1 2 策略模式的结构通常包括以下组成部分 1 3
  • Effective C++——尽可能使用const

    const允许指定一个语义约束 也就是指定一个 不该被改动 的对象 而编译器会强制实施这项约束 只要保持某个值不变是事实 就应该说出来 以获得编译器的协助 保证不被违反 const与指针 注意const的写法 const char p p可

随机推荐

  • 关于如何使用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 它会返回一个整数 表示人物的健康程度 由