[Effective C++]条款35 virtual函数替换方案

本文初发于 “天目中云的小站”,同步转载于此。

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

我们都知道使用virtual函数是有代价的, 它会带来额外的开销, 譬如占用内存, 降低效率, 不好控制安全性等问题, 因此如果我们想构建一个逻辑缜密且标准的项目, 可以考虑一些virtual函数的替换方案, 或许会得到更强的安全性或更高的效率, 本条款将会介绍一些替换方案给你, 也许你会觉得复杂了许多, 但是这是在提升我们对各种程序设计方案的理解, 是对我们内核的修炼.

先引入书中的一个例子, 假设我们要建立一个游戏人物类, 基类希望用virtual函数继承一个计算人物健康程度的成员函数healthValue, 我们之后都会围绕这个例子来讨论 :

class GameCharacter {    // 游戏人物基类
public:
  virtual int healthValue() const;        // 返回一个人物健康值, 基类也会提供一个缺省版本的计算函数
  ...                                    
};

这样的设计十分合理, 但不一定就是最可控最高效的方案, 让我们来考虑一些其他的解法 :


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

这个方案并没有不使用virtual函数, 而是将virtual函数从public/protect转为了private, 进而提升了vitual函数的可控性和安全性.

这一切的前提建立在C++允许private virtual函数可以被派生类继承并重写之上, 有了这个共识我们再来看下面的分析.

先把上面两个专有名词介绍一下 :

  • Template Method模式

    一种行为型设计模式,用于定义一个算法的骨架,并允许派生类在不改变算法结构的前提下重新定义算法的某些步骤。

    其内核在于 : 将算法的固定部分写在基类中,而将可变部分(即具体实现)延迟到派生类中. 就是说基类部分提供固定的整体流程, 做出一些必要的准备工作(如加解锁, 记录日志, 验证约束条件等), 然后具体计算方式由派生类实现.

  • Non-Vitual Interface(简称NVI, 非虚接口)

    这是对上述设计模式的一种实现手法, 这种手法主张所有的virtual函数应该几乎总是private, 通过基类的non-virtual函数作为接口, 也就是NVI, 去通过动态绑定调用派生类的virtual函数, 以实现对virtual函数的控制与规范化.

让我们来看代码理解 :

class GameCharacter {
public:
  int healthValue() const               // 一个non-virtual函数
  {                                     
    ...                                 // 事前工作 : 加锁/写日志/验证约束条件等
    int retVal = doHealthValue();       // 真正的工作逻辑, 可以继承自派生类, 也可以使用自己的缺省版本
    ...                                 // 事后工作 : 解锁/写日志等
    return retVal;
  }
  ...

private:								// 在基类和派生类中都是private, 唯一通过基类的healthValue调用
  virtual int doHealthValue() const     // 可以被派生类重写
  {
    ...                                 // 缺省版本
  }                                   
};

这种设计模式可以确保virtual函数被正确合理的使用, 只要其内部工作逻辑正确.

如何使用? “令客户通过调用基类的public non-virtual成员函数间接调用派生类的private virtual成员函数”, 我们一般称这种public non-virtual成员函数virtual函数的外覆器(wrapper).

NVI手法涉及在派生类内重新定义private virtual函数, 这种虚函数不能被任何对象调用, 但其内核是合理的, 书中认为 :

  • base class保留诉说"函数何时被调用"的权利, 以控制调用.
  • derived classes则被赋予"如何实现机能"的控制能力, 以控制实现.

并且虽然NVI手法主张都用private virtual函数, 但是也并非完全必要, 因为确实有些情况必须要直接调用(比如子类成员函数要先调用父类对应成员函数的设计), 我们可以灵活变通, 只要使用手法保持一致即可.


藉由function实现Strategy模式

相比于上一种方案其实还在使用virtual函数机制, 只是在将其变得更加可控安全, 这个方案则是完全摒弃了这种机制, 以一种另辟蹊径的方式实现了类似public virtual函数的功能.

还是先了解专有名词 :

  • Strategy模式

    这也是一种行为型设计模式,定义了一系列算法(策略),并将每种算法封装起来,使它们可以互相替换而不影响使用这些算法的客户端代码. 其核心就在于, 通过将算法或行为抽象为独立的策略类,允许在运行时动态地更改对象的行为,而无需修改其代码.

  • function

    这是C++11引入的新特性, 意在优化类函数方法的调用, 是一个"可调用物", 具体细节不再阐释. 这里主要讲解其如何实现Strategy模式, 简单来说就是基类不再定义virtual函数, 而是定义一个non-virtual函数, 其内部调用一个函数方法, 这个函数方法实际是基类内部的一个function成员变量, 它可以是基类给出的缺省方法, 也可以是通过构造函数参数传入的外部function函数方法, 这种function可以被视为一种Strategy(策略). 光看比较难以理解, 我们看代码 :

class GameCharacter;                                 
int defaultHealthCalc(const GameCharacter& gc);      // 在外部写一个默认方法

class GameCharacter {
public:
   // function的定义不再解释, 这里主要是通过typdef简化类型的书写
   typedef std::function<int (const GameCharacter&)> HealthCalcFunc;

   explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) // 这里指定默认方法, 也可以传入自定义的方法
   : healthFunc(hcf)
   {}

   int healthValue() const
   { return healthFunc(*this); }   // 这里调用内部的函数方法

   ...

private:
  HealthCalcFunc healthFunc; 	   // 保存function的成员变量
};

这样的实现方法实际上是将健康值的计算和类型本身完全解耦, 客户需要在使用时传入希望的健康值计算策略, 本质是将策略赋予某个对象, 而非virtual函数由类型决定策略, 让我们看如下代码 :

class EvilBadGuy: public GameCharacter {  // 创建一个恶魔坏男孩的派生类
public:
  explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc)
  : GameCharacter(hcf)   // 调用基类构造函数, 将方法传进去
  { ... }
};

int loseHealthQuickly(const GameCharacter&);    // 快速流失血量的健康值计算方法

EvilBadGuy ebg(loseHealthQuickly);             // 将上面自定义的健康值计算方法赋予创建出来的ebg对象

优势说明

这种方法虽然不及virtual函数来得简单便捷, 但运行效率和安全性上有了很大提升, 并且在很多方面都展现出了极为惊人的弹性! 让我们逐一见识 :

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

    int loseHealthQuickly(const GameCharacter&);    // 快速流失健康值的健康值计算方法
    int loseHealthSlowly(const GameCharacter&);     // 缓慢流失健康值的健康值计算方法
    
    EvilBadGuy ebg1(loseHealthQuickly);            
    EvilBadGuy ebg2(loseHealthSlowly); 
    

    这样相同类型的不同对象就可以设置不同的计算方法了!

  • 一个实体可以在不同的运行时期可以有不同的健康计算函数.

    class GameCharacter {
    public:
     	...
     	void setHealthCalculator(HealthCalcFunc hcf);
      	...
    };
    // --------------------------------------//
    EvilBadGuy ebg(loseHealthSlowly);  					// 初始设置为缓慢流失
    ebg.setHealthCalculator(loseHealthQuickly);			// 例如中途ebg中毒了, 更改设置为快速流失
    

    我们可以在GameCharacter中提供一个设置计算方法的函数, 这样便可以中途更改方法了!

  • funtion本身可以带来极大的隐式转换弹性.

    function可以接受几乎所有的可调用物, 包括普通函数, 仿函数, 成员函数, lambda表达式, 只要参数与返回值对应即可, 并且这里的对应是支持隐式转换的! 也就是说就算写的funtion类型是function<int(const GameCharacter&)>, 它依然可以接受一个short(const GameCharacter&)类型的函数, 因为short可以隐式转换为int.

    这是非常强大的功能, 可以让我们面对不同的使用场景使用不同的方式传入计算方法, 让我们通过以下代码来深入了解 :

    short calcHealth(const GameCharacter&);      	// 1.一个返回short类型的普通计算函数
    
    struct HealthCalculator {                        
        int operator()(const GameCharacter&) const  // 2.一个用于计算的仿函数
        {...}
    };
    
    class GameLevel {    // 这个类代表游戏等级, 可能会根据不同的等级返回不同的计算策略
    public:
      float health(const GameCharacter&) const;     // 3.一个返回float类型的成员函数
      ...                                           
    };                                               
    

    于是我们可以这样子使用它们 :

    class EvilBadGuy: public GameCharacter {      // 恶魔坏男孩
      ...
    };
    class EyeCandyLady:   public GameCharacter {  // 大眼甜心
      ...                                             
    };                                                                                                   
    //-----------------------------------------//
    
    EvilBadGuy ebg1(calcHealth);                    	// 1.传入普通函数
    EyeCandyCharacter ecc1(HealthCalculator());     	// 2.传入仿函数
    
    GameLevel currentLevel;		// 获取当前等级
    ...
    EvilBadGuy ebg2(                                  	// 3.传入成员函数
        // 因为成员函数有隐藏的this指针, 这里用bind将当前等级绑定到成员函数中
      	std::bind(&GameLevel::health, currentLevel, _1)                      
    );
    
    EyeCandyCharacter ecc2([](const GameCharacter&){...});  // 4.传入lambda表达式
    

以上便是该手法的优势所在, 这些都是光凭virtual函数所无法实现的功能.


古典Strategy模式

这个是书中简单介绍的方法, 简单来说就是上一个方案的加重版, 原本只是传入一个健康计算函数, 现在改成传入一个健康计算类, 其可以派生出loseHealthSlowlyloseHealthQuickly等派生类, 实际就是将本体系中的virtual成员函数, 改为一个分离的继承体系中的virtual成员函数. 这个方案的优势在于更加体系化, 使其可能可以纳入外部现成的既有健康计算体系, 拿别人早就经过时间验证过的成果未必不是一种更安全更高效的方法. 代码如下, 其他不再详述 :

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;
};

请记住 :

  • virtual函数固然有其便捷多态的优势所在, 但其依然会有内存花销, 运行花销, 安全性问题, 我们可以思考一些替换方案来解决这些问题并且带来一些独特的额外优势.
  • 藉由Non-Vitual Interface手法实现Template Method模式, 使得virtual函数的使用更加规范可控, 提高了安全性.
  • 藉由function实现Strategy模式, 使得改为以策略赋予对象, 在使用时有巨大的弹性空间.
  • 古典Strategy模式可以引入一些前人早就构建好的体系, 基本可以保证安全高效.

by 天目中云

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

使用Docker启用MySQL8.0.11

目录 一、Docker减小镜像大小的方式 1、基础镜像选择 2、减少镜像层数 3、清理无用文件和缓存 4、优化文件复制&#xff08;COPY和ADD指令&#xff09; 二、Docker镜像多阶段构建 1、什么是dockers镜像多阶段构建 1.1 概念介绍 1.2 构建过程和优势 2、怎样在Dockerfil…

【微信小程序开发 - 3】:项目组成介绍

文章目录 项目组成介绍项目的基本组成结构小程序页面的组成部分JSON配置文件的作用app.json文件project.config.json文件sitemap.json文件页面的 .json 配置文件新建小程序页面修改项目首页 XWML模板XWML 和 HTML 的区别 WXSS样式WXSS 和 CSS 的区别 .js文件 项目组成介绍 项目…

springboot的项目创建和常用注解

创建springboot项目&#xff1a; 首先更改一下url&#xff0c;点击小齿轮改成https://start.aliyun.com/ 首先在选模块的时候选上SpringWeb&#xff0c;然后jdk1.8对应的springboot版本是2.6.13或者2.7.6 pom.xml: 用1.8的jdk&#xff0c;mybatis的包版本不能太高 <!-- …

flask_socketio 以继承 Namespace方式实现一个网页聊天应用

点击进入上一篇&#xff0c;可作为参考 实验环境 python 用的是3.11.11 其他环境可以通过这种方式一键安装&#xff1a; pip install flask3.1.0 Flask-SocketIO5.4.1 gevent-websocket0.10.1 -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple pip list 详情如下&am…

笔记本重装系统教程【详细教程】

一、装机前说明 各位有装机需求的伙伴&#xff0c;请根据自己的电脑配置选择合适操作系统&#xff0c;可以实现自己装机的伙伴&#xff0c;相信大家这点可以确认好。 ———————————————————————— 我的配置&#xff1a; 我的电脑是联想拯救者lenovoY7000…

uniapp入门 01创建项目模版

0安装 hbuilder x 标准版 1.创建模版工程 2.创建官方 案例工程 index.uvuewen 文件解析 <!-- 模版 标签 --> <template><view></view></template><!-- 脚本 --> <script>export default {data() {return {}},onLoad() {},methods:…

ARCGIS国土超级工具集1.2更新说明

ARCGIS国土超级工具集V1.2版本&#xff0c;功能已增加至47 个。在V1.1的基础上修复了若干使用时发现的BUG&#xff0c;新增了"矢量分割工具"菜单&#xff0c;同时增加及更新了了若干功能&#xff0c;新工具使用说明如下&#xff1a; 一、勘测定界工具栏更新界址点成果…

Vue3源码笔记阅读1——Ref响应式原理

本专栏主要用于记录自己的阅读源码的过程,希望能够加深自己学习印象,也欢迎读者可以帮忙完善。接下来每一篇都会从定义、运用两个层面来进行解析 定义 运用 例子:模板中访问ref(1) <template><div>{{str}}</div> </template> <script> impo…

[react] 优雅解决typescript动态获取redux仓库的类型问题

store.getState()是可以获取总仓库的 先拿到函数的类型 再用ReturnType<T> 它是 TypeScript 中的一个内置条件类型&#xff0c;用于获取某个函数类型 T 的返回值类型 代码 // 先拿总仓库的函数类型type StatefuncType typeof store.getState;//再拿函数类型T的返回值类…

【Qt】QWidget中的常见属性及其功能(一)

目录 一、 enabled 例子&#xff1a; 二、geometry 例子&#xff1a; window fram 例子 &#xff1a; 四、windowTiltle 五、windowIcon 例子&#xff1a; qrc机制 创建qrc文件 例子&#xff1a; qt中的很多内置类都是继承自QWidget的&#xff0c;因此熟悉QWidget的…

R语言的字符串操作

【图书推荐】《R语言医学数据分析实践》-CSDN博客 《R语言医学数据分析实践 李丹 宋立桓 蔡伟祺 清华大学出版社9787302673484》【摘要 书评 试读】- 京东图书 (jd.com) R语言医学数据分析实践-R语言的数据结构-CSDN博客 在R语言中&#xff0c;字符串是一种表示文本数据的数…

webGL硬核知识:图形渲染管渲染流程,各个阶段对应的API调用方式

一、图形渲染管线基础流程概述 WebGL 的图形渲染管线大致可分为以下几个主要阶段&#xff0c;每个阶段都有其特定的任务&#xff0c;协同工作将 3D 场景中的物体最终转换为屏幕上呈现的 2D 图像&#xff1a; 顶点处理&#xff08;Vertex Processing&#xff09;阶段&#xff1…

《深入浅出Apache Spark》系列⑤:Spark SQL的表达式优化

导读&#xff1a;随着数据量的快速增长&#xff0c;传统的数据处理方法难以满足对计算速度、资源利用率以及查询响应时间的要求。为了应对这些挑战&#xff0c;Spark SQL 引入了多种优化技术&#xff0c;以提高查询效率&#xff0c;降低计算开销。本文从表达式层面探讨了 Spark…

创建项目以及本地仓库和远程仓库并上传项目

创建项目以及本地仓库和远程仓库并上传项目 其详细流程如下&#xff1a; 1、本地创建项目 2、创建本地仓库&#xff08;若使用idea在创建项目时选择了创建.git本地仓库&#xff0c;则此步骤省略&#xff09; 进入到你需要上传的项目的目录下&#xff0c;右键找到Git Bah He…

快速解决oracle 11g中exp无法导出空表的问题

在一些生产系统中&#xff0c;有些时候我们为了进行oracle数据库部分数据的备份和迁移&#xff0c;会使用exp进行数据的导出。但在实际导出的时候&#xff0c;我们发现导出的时候&#xff0c;发现很多空表未进行导出。今天我们给出一个快速解决该问题的办法。 一、问题复现 我…

MySQL八股-MVCC入门

文章目录 当前读&#xff08;加锁&#xff09;快照读&#xff08;不加锁&#xff09;MVCC隐藏字段undo-log版本链A. 第一步B.第二步C. 第三步 readview MVCC原理分析RCA. 先来看第一次快照读具体的读取过程&#xff1a;B. 再来看第二次快照读具体的读取过程: RR隔离级别 当前读…

【已解决】启动此实时调试器时未使用必需的安全权限。要调试该进程,必须以管理员身份运行此实时调试器。是否调试该进程?

【已解决】启动此实时调试器时未使用必需的安全权限。要调试该进程&#xff0c;必须以管理员身份运行此实时调试器。是否调试该进程? 目录一、前言二、具体原因三、解决方法 目录 报错截图 一、前言 进行应用程序开发时&#xff0c;需要对w3wp进行附加调试等场景&#xff…

基于Qt的登陆界面设计

目标 自由发挥登录界面的应用场景&#xff0c;实现一个登录窗口的界面。 要求&#xff1a;每行代码都要有注释 代码 // 设置窗口大小为600x400像素 this->resize(600,400); // 设置窗口标题为"TheWitcher 巫师3&#xff1a;狂猎" this->setWindowTitle(&qu…

Elasticsearch 8.x 集成与 Java API 使用指南

目录 背景 版本区别 安装elaticsearch8.x服务 启动es服务 安装es管理平台 项目集成 pom.xml文件引入依赖 application.yml配置 ES初始化配置类实现 ES8.x常用API实现 1.判断es索引是否存在 2.删除索引 3.创建索引 4.新增文档 5.更新文档 6.根据id查询文档 7.根…

EfficientNet模型Pytorch版本具体实现

EfficientNet模型原理&#xff1a;EfficientNet&#xff1a;对模型深度、宽度和分辨率的混合缩放策略-CSDN博客 一、激活函数&#xff1a; EfficientNet模型使用了Swish激活函数而不是更常见的Relu激活函数 1、公式定义 Swish(x) x * sigmoid(x)是一个平滑的非线性激活函数…