[翻译] Effective C++, 3rd Edition, Item 35: 考虑可选的 virtual functions(虚拟函数)的替代方法(下)

限时加码!20+主流AI编程工具免费用 购周边加赠Coding Plan Lite,Claude Code、Cursor等即刻畅享,学习进阶更高效! 阅读详情

(点击此处,接上篇)

The Strategy Pattern via tr1::function(经由 tr1::function 实现的策略模式)

一旦你习惯了 templates(模板)和 implicit interfaces(隐式接口)(参见 Item 41)的应用,function-pointer-based(基于函数指针)的方法看上去就有些死板了。健康值的计算为什么必须是一个 function(函数),而不能是某种简单的行为类似 function(函数)的东西(例如,一个 function object(函数对象))?如果它必须是一个 function(函数),为什么不能是一个 member function(成员函数)?为什么它必须返回一个 int,而不是某种能够转型为 int 的类型?

如果我们用一个 tr1::function 类型的对象代替一个 function pointer(函数指针)(诸如 healthFunc),这些约束就会消失。就像 Item 54 中的解释,这样的对象可以持有 any callable entity(任何可调用实体)(例如,function pointer(函数指针),function object(函数对象),或 member function pointer(成员函数指针)),这些实体的标志性特征就是兼容于它所期待的东西。我们马上就会看到这样的设计,这次使用了 tr1::function

class GameCharacter;                                 // as before
int defaultHealthCalc(const GameCharacter& gc);      // as before

class GameCharacter {
public:
   // HealthCalcFunc is any callable entity that can be called with
   // anything compatible with a GameCharacter and that returns anything
   // compatible with an int; see below for details
   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 是一个 tr1::function instantiation(实例化)的 typedef。这意味着它的行为类似一个普通的 function pointer(函数指针)类型。我们近距离看看 HealthCalcFunc 究竟是一个什么东西的 typedef:

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

这里我突出了这个 tr1::function instantiation(实例化)的“target signature(目标识别特征)”。这个 target signature(目标识别特征)是“取得一个引向 const GameCharacter 的 reference(引用),并返回一个 int 的函数”。这个 tr1::function 类型的(例如,HealthCalcFunc 类型的)对象可以持有兼容于这个 target signature(目标识别特征)的 any callable entity(任何可调用实体)。兼容意味着这个实体的参数能够隐式地转型为一个 const GameCharacter&,而它的返回类型能够隐式地转型为一个 int

与我们看到的最近一个设计(在那里 GameCharacter 持有一个指向一个函数的指针)相比,这个设计几乎相同。仅有的区别是目前的 GameCharacter 持有一个 tr1::function 对象——指向一个函数的 generalized(泛型化)指针。除了达到“clients(客户)在指定健康值计算函数时有更大的灵活性”的效果之外,这个变化是如此之小,以至于我宁愿对它视而不见:

short calcHealth(const GameCharacter&);          // health calculation
                                                 // function; note
                                                 // non-int return type

struct HealthCalculator {                        // class for health
  int operator()(const GameCharacter&) const     // calculation function
  { ... }                                        // objects
};

class GameLevel {
public:
  float health(const GameCharacter&) const;      // health calculation
  ...                                            // mem function; note
};                                               // non-int return type


class EvilBadGuy: public GameCharacter {         // as before
  ...
};
class EyeCandyCharacter:   public GameCharacter {  // another character
  ...                                              // type; assume same
};                                                 // constructor as
                                                   // EvilBadGuy


EvilBadGuy ebg1(calcHealth);                       // character using a
                                                   // health calculation
                                                   // function


EyeCandyCharacter ecc1(HealthCalculator());        // character using a
                                                   // health calculation
                                                   // function object

GameLevel currentLevel;
...
EvilBadGuy ebg2(                                   // character using a
  std::tr1::bind(&GameLevel::health,               // health calculation
          currentLevel,                            // member function;
          _1)                                      // see below for details
);

就个人感觉而言:我发现 tr1::function 能让你做的事情是如此让人惊喜,它令我浑身兴奋异常。如果你没有感到兴奋,那可能是因为你正目不转睛地盯着 ebg2 的定义并对 tr1::bind 的调用会发生什么迷惑不解。请耐心地听我解释。

比方说我们要计算 ebg2 的健康等级,应该使用 GameLevel class(类)中的 health member function(成员函数)。现在,GameLevel::health 是一个被声明为取得一个参数(一个引向 GameCharacter 的引用)的函数,但是它实际上取得了两个参数,因为它同时得到一个隐式的 GameLevel 参数——指向 this。然而,GameCharacters 的健康值计算函数只取得单一的参数:将被计算健康值的 GameCharacter。如果我们要使用 GameLevel::health 计算 ebg2 的健康值,我们必须以某种方式“改造”它,以使它适应只取得唯一的参数(一个 GameCharacter),而不是两个(一个 GameCharacter 和一个 GameLevel)。在本例中,我们总是要使用 currentLevel 作为 GameLevel 对象来计算 ebg2 的健康值,所以每次调用 GameLevel::health 计算 ebg2 的健康值时,我们就要 "bind"(凝固)currentLevel 来作为 GameLevel 的对象来使用。这就是 tr1::bind 的调用所做的事情:它指定 ebg2 的健康值计算函数应该总是使用 currentLevel 作为 GameLevel 对象。

我们跳过一大堆的细节,诸如为什么 "_1" 意味着“当为了 ebg2 调用 GameLevel::health 时使用 currentLevel 作为 GameLevel 对象”。这样的细节并没有什么启发性,而且它们将转移我所关注的基本点:在计算一个角色的健康值时,通过使用 tr1::function 代替一个 function pointer(函数指针),我们将允许客户使用 any compatible callable entity(任何兼容的可调用实体)。很酷是不是?

The "Classic" Strategy Pattern(“经典的”策略模式)

如果你比 C++ 更加深入地进入 design patterns(设计模式),一个 Strategy 的更加习以为常的做法是将 health-calculation function(健康值计算函数)做成一个独立的 health-calculation hierarchy(健康值计算继承体系)的 virtual member function(虚拟成员函数)。做成的 hierarchy(继承体系)设计看起来就像这样:

如果你不熟悉 UML 记法,这不过是在表示当把 EvilBadGuyEyeCandyCharacter 作为 derived classes(派生类)时,GameCharacter 是这个 inheritance hierarchy(继承体系)的根;HealthCalcFunc 是另一个带有 derived classes(派生类)SlowHealthLoserFastHealthLoser 的 inheritance hierarchy(继承体系)的根;而每一个 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 pattern(策略模式)实现的人可以很快地识别出来,再加上它提供了通过在 HealthCalcFunc hierarchy(继承体系)中增加一个 derived class(派生类)而微调已存在的健康值计算算法的可能性。

Summary(概要)

这个 Item 的基本建议是当你为尝试解决的问题寻求一个设计时,你应该考虑可选的 virtual functions(虚拟函数)的替代方法。以下是对我们考察过的可选方法的一个简略的回顾:

  • 使用 non-virtual interface idiom (NVI idiom)(非虚拟接口惯用法),这是用 public non-virtual member functions(公有非虚拟成员函数)包装可访问权限较小的 virtual functions(虚拟函数)的 Template Method design pattern(模板方法模式)的一种形式。
  • function pointer data members(函数指针数据成员)代替 virtual functions(虚拟函数),一种 Strategy design pattern(策略模式)的显而易见的形式。
  • tr1::function data members(数据成员)代替 virtual functions(虚拟函数),这样就允许使用兼容于你所需要的东西的 any callable entity(任何可调用实体)。这也是 Strategy design pattern(策略模式)的一种形式。
  • virtual functions in another hierarchy(另外一个继承体系中的虚拟函数)代替 virtual functions in one hierarchy(单独一个继承体系中的虚拟函数)。这是 Strategy design pattern(策略模式)的习以为常的实现。

这不是一个可选的 virtual functions(虚拟函数)的替代设计的详尽无遗的列表,但是它足以使你确信这些是可选的方法。此外,它们之间互为比较的优劣应该使你考虑它们时更为明确。

为了避免陷入 object-oriented design(面向对象设计)的习惯性道路,时不时地给车轮一些有益的颠簸。有很多其它的道路。值得花一些时间去考虑它们。

Things to Remember

  • 可选的 virtual functions(虚拟函数)的替代方法包括 NVI 惯用法和 Strategy design pattern(策略模式)的各种变化形式。NVI 惯用法本身是 Template Method design pattern(模板方法模式)的一个实例。
  • 将一个机能从一个 member function(成员函数)中移到 class(类)之外的某个函数中的一个危害是 non-member function(非成员函数)没有访问类的 non-public members(非公有成员)的途径。
  • tr1::function 对象的行为类似 generalized function pointers(泛型化的函数指针)。这样的对象支持所有兼容于一个给定的目标特征的 callable entities(可调用实体)。
从跳表无锁插入到元数据原子发布:现代 C++ 存储内核如何跨越物理 I/O 与 CPU 内存模型 在现代工业级 LSM-Tree 存储引擎的内核设计中,C++ 工程师对“无锁化(Lock-Free)”与“纳秒级确定性”的极致偏执,几乎被雕刻进了每一行代码里。为了抹平动态内存分配器的碎片与互斥锁竞争,我们在写入路径上采用紧凑的 Arena 内存池与无锁单写多读跳表(),将插入操作的延迟死死压制在 CPU 缓存流水线的物理极限;为了让前台点查(Point Get)彻底告别昂贵的全局锁争用与特权级上下文切换,引擎巧妙融合了线程局部缓存( 阅读详情

相关推荐

[翻译] Effective C++, 3rd Edition, Item 35: 考虑可选virtual functions虚拟函数)的替代方法(上)

Item 35: 考虑可选virtual functions虚拟函数)的替代方法作者:Scott Meyers译者:fatalerror99 (iTePubs Nirvana)发布:http://blog.csdn.net/fatalerror99/现在你工作在一个视频游戏上,你在游戏中为角色设计了一个 hierarchy(继承体系)。你的游戏中有着变化多端的恶劣环境,角色

艺术编程 4392

Qwen3-ForcedAligner-0.6B 私有化部署实战:从模型离线、Docker封装到会议原声精准回听

text=text,模型离线下载版本固定SHA256目录规划环境隔离Docker封装GPU绑定健康检查长音频切片全局时间恢复临时文件清理日志脱敏离线验证模型升级快速回滚这些内容看起来和“模型算法”关系不大,却决定了一个 AI 模块究竟只能停留在 Demo,还是能长期运行在真实系统里。已经知道说了什么↓准确找到它什么时候说逐字稿点击回听搜索定位纪要复核会议资料追溯从产品界面上看,它可能只是用户点击一句话以后,播放器准确跳到了对应位置。

shxjnpl的博客 174

C++——C++基础】

本文系统介绍了C++中的命名空间、输入输出、缺省参数、函数重载与引用机制。命名空间有效避免标识符冲突,通过namespace和::实现作用域隔离;C++输入输出流(cin/cout)基于std命名空间,支持自动类型识别;缺省参数需从右向左连续定义,且声明中指定默认值;函数重载通过形参差异实现多态;引用作为变量别名,无额外内存开销,广泛用于传参与返回值,提升效率,但不可改变指向,且不能返回局部变量引用。

SHARK_pssm的博客 172

顺序表(Sequential List)详解:从数组到 C 语言实现

顺序表是用连续存储单元依次存储数据元素的线性结构,数组则是其最常见的实现方式,两者本质相同、仅视角不同——数组关注物理存储,顺序表关注逻辑关系。顺序表支持随机访问 $O(1)$,但插入删除需移动元素,效率为 $O(n)$。本文用 C 语言静态数组完整实现了顺序表的初始化、增删改查操作,帮助初学者在不涉及指针的前提下理解其核心原理,为后续学习动态顺序表打下基础。

Guangyu536的博客 290

C++】 定时器进阶:多层时间轮原理与任务迁移

单层时间轮虽然能够以较低的开销管理大量定时任务,但当任务延迟时间跨度很大时,要么需要大量槽位,要么需要额外维护轮数。多层时间轮通过将不同时间范围的任务放到不同层级中,实现类似“秒针、分针、时针”的分级管理。本文主要介绍多层时间轮为什么出现、各层如何划分、定时任务如何选择层级,以及高层任务如何逐步迁移到低层并最终执行。

qq_64148519的博客 114

附录 C:AOSP 不在公共框架 API 中采用 Kotlin 的原因

以及源码。契约生命周期约束设备发布后会锁定某个 SDK 版本长达接近十年。每一条签名,必须兼容未来所有 kotlinc 版本、Kotlin 元数据格式变更、标准库改动。内部 Kotlin 不受该限制:编译器签名变化可以通过下一次全量重编译抹平。二进制 ABI 映射差异Java 源码声明与 JVM 签名一一对应。Kotlin 一条源码会产出一组 JVM 产物:重载方法、名字混淆、伴生对象访问器、合成辅助函数、Continuation 参数、元数据块;组合结果完全依赖编译器。

dengfuma的专栏 722

STM32 USART 详解(八):C I/O 串口重定向原理与实现

本文深入解析STM32串口重定向C标准I/O的核心原理与实现。文章从printf/scanf底层机制入手,剖析数据流向,并提供fputc与fgetc的完整重定向代码。同时深度解析换行符及输入机制问题,最后通过完整测试验证数据流~

2502_91499791的博客 130

​​​​​​​第 4 节 如何用 C++ 设计一个 AI 推理 SDK 的核心接口

AI推理SDK核心接口应遵循“环境→模型→执行器→张量”四层抽象,采用不透明指针保证ABI稳定,以C API为底座、C++ API为封装,统一错误处理,并权衡张量、内存、异步、线程安全与插件扩展。参考ONNXRuntime、TensorRT、OpenVINO设计,实现易用、高性能、可扩展的工业级接口。

Question_123456的博客 515

DeepSeek Harness 系列(02):万物皆插件——Cordis 核心设计深度解读

dsh 的所有能力都建立在 Cordis 插件框架之上。这篇文章结合真实源码,深度解读 Cordis 五个核心机制:Plugin、Context、Service、Event、Effect——每个机制讲清楚「是什么、怎么用、为什么这么设计」。读完之后,dsh 的一切都会豁然开朗。

Cheson的专栏 524

Type-C快充取电芯片工作流程:从适配器识别到电压输出

PW6607是一颗USB电源传输接收SINK端控制器芯片,PW6607可以从PD/QC适配器电源请求PW6606设定的电压。当连接USB电源时,PD/QC协议自动通讯,完成PW6607设定的电压请求通讯,并输出通讯设定的电压。PW6606内置 PD 通讯模块和QC通讯,集成度高,外围简洁。PD协议优先级高于QC协议,仅当PD诱骗异常或者前端不支持PD时,才会进行 QC诱骗。PD 握手会优先寻找适配设置电压的 PDO 挡位,当没有需要的挡位时,自动降低电压挡位诱骗。5V,9V,12V和20V(PD);

MILLET6的博客 122

实现pytorch resnet18功能(二,c++ cudnn)

/输出32*32*32-----------------------显然输入也是32*32*32。, _ci(ci), _co(co), _h(h), _w(w), _jjh(juanjihe), _pad(pading) {//int stride=2,juanjihe=1时,pading=0;原因是残差块中最后一步只实现了相加,并没有实现relu,所以这个架构中,所有残差块类都跟有relu!

ganggangwawa的博客 197

VSCode C/C++环境设置

本文介绍了在VSCode中配置C/C++开发环境的完整流程:通过下载MinGW-w64工具链,安装C/C++及C/C++ Runner扩展,配置tasks.json与launch.json实现编译、运行与调试。重点说明了使用快捷键(如Ctrl+Shift+B编译、F5调试)及解决中文路径导致GDB报错的方法。适合初学者快速搭建高效C/C++开发环境。

念致达的博客 152

Tera Term 鸿蒙 PC 适配全记录:用 ArkUI 与 Native C++ 重建多协议终端工作流

开源鸿蒙PC社区推出Tera Term终端模拟器适配版本,基于ArkTS/ArkUI与C++ Native分层架构,实现TCP、Telnet、SSH及串口通信功能。项目在HarmonyOS PC真机上成功验证五类核心场景:应用启动、网络连接、双向输入输出、多会话独立管理与终端渲染。通过Node-API桥接前后端,保留原生终端语义,不依赖Win32兼容层,为嵌入式与运维用户提供稳定高效的跨平台终端体验。

稻草人 1万+

C++速通2

int a = 3;int b = 4;// 声明名字空间int main()int a = 1;// 1// ::表示全局作用域 2// 3// 4return 0;```一个函数使用virtual关键字修饰,就是虚函数,虚函数函数覆盖的前提。在QtCreator中使用斜体字。```cpppublic:// 虚函数cout << "动物爱吃饭" << endl;

qq_63858767的博客 441

CCFCSP算法专栏:关于二分

left = mid + 1 # mid 可能是答案,但先往右探else:return left - 1 # 注意减 1为什么 return left - 1?因为这里的left是“第一个 > target 的位置”,所以最后一个 <= target 的位置是left - 1。场景区间循环条件更新方式返回值精确查找[l, r]l <= rl=mid+1r=mid-1循环内 return mid,否则 -1左边界(第一个 >= target)[l, r)l < rl=mid+1。

Chester_1999的博客 189

ARM64 Linux 6.10 内核驱动(14):drivers/base/platform.c:板级设备主战场

本文详解 drivers/base/platform.c 在 QEMU virt + ARM64 平台下如何实现平台设备驱动模型。核心是将无自动注册能力的 MMIO 设备通过 platform_bus_init 建立伪总线,设备通过 platform_device_add 挂载至该总线,驱动则通过 __platform_driver_register 注册并绑定。匹配机制优先依赖 DT 的 compatible(of_match_table),其次为旧式 id_table 和名称匹配。platform_pr

T1mzhou的博客 176

【408数据结构 03】线性表与顺序表:C++手写SeqList

顺序表用一段地址连续的存储单元依次存放线性表元素。假设每个元素占个字节,第一个元素地址为LOC(a1),则第i这就是 408 常考的随机访问公式。下标从 0 还是 1 开始408 题目中位置i通常从 1 开始,代码里数组下标从 0 开始,注意转换。插入位置范围是容易漏掉length + 1,也就是表尾插入。删除位置范围是删除不能到length + 1。插入从后往前移动,删除从前往后移动方向搞反会覆盖数据。平均移动次数公式要记牢插入n/2,删除(n-1)/2。顺序表长度和容量不是一回事。

xiangyun61的博客 254

C++ std::queue<float> 完整教程

std::queue是front()back()push()pop()

谢谢大家的关注和点赞!这里只有纯纯的知识干货,没有一句废话。希望能实实在在帮到大家~要是觉得有用,别忘了给我点支持哟,你 80

C++第一课

你暂时不用背它。

qq_56657939的博客 139
上一篇: [翻译] Effective C++, 3rd Edition, Item 35: 考虑可选的 virtual functions(虚拟函数)的替代方法(上)
下一篇: [翻译] Effective C++, 3rd Edition, Item 36: 绝不要重定义一个 inherited non-virtual function(通过继承得到的非虚拟函数)
fatalerror99
博客等级 码龄24年 166粉丝 20原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值