[Effective C++]条款38-39 复合和private继承

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

条款38 : 通过复合塑膜出has-a或"is-implemented-in-terms-of"

在条款32中我们认识了public继承意味着is-a, 本条款将会认识两个新的关系, 均可通过"复合"这一操作实现出来.


复合

所谓复合, 就是某种类型的对象内含其他类型的对象, 其实非常容易理解, 我们通过代码理解 :

class Address { ... };           
class PhoneNumber { ... };class Person {
public:...
private:std::string name;              // 复合std::stringAddress address;               // 复合AddressPhoneNumber voiceNumber;       // 复合PhoneNumberPhoneNumber faxNumber;          
};

Person类便是复合了AddressPhoneNumber两个类.


两种意义

使用复合手法在不同领域的对象上, 有不同的意义, 我们先来定义两个领域 :

  • 应用域 : 当一个内置对象的目的是"这个类能做到什么(what)", 它属于应用域, 上面的地址和电话便是如此.
  • 实现域 : 当一个内置对象的目的是"这个类如何做到(how)", 它属于实现类.

在设置不同域的对象其代表的意义也不同, 让我们看下面两种意义 :

  • has-a(有一个)

    当对象属于应用域, 如果A复合了B, 则代表A有一个B, A可以做出B的所有行为, 但不强制, 例如人有一个家.

  • is-implemented-in-terms-of(根据某物实现出)

    当对象属于实现域, 如果A复合了B, 则代表A根据B实现出来, A想要实现自己的功能要借助B的功能, 例如标准库中的unordered_set根据HashTable实现出来.

    我如果不想用HashTable实现set, 也可以用list来实现, 书中给出的例子是这样的 :

    template<class T>                 
    class Set {
    public:bool member(const T& item) const;void insert(const T& item);void remove(const T& item);std::size_t size() const;private:std::list<T> rep;                 // 复用list实现另一个版本的set.
    };
    

    于是Set就可以依靠list提供的机能来实现自己的功能 :

    template<typename T>
    bool Set<T>::member(const T& item) const
    {return std::find(rep.begin(), rep.end(), item) != rep.end();
    }template<typename T>
    void Set<T>::insert(const T& item)
    {if (!member(item)) rep.push_back(item);
    }template<typename T>
    void Set<T>::remove(const T& item)
    {typename std::list<T>::iterator it =               std::find(rep.begin(), rep.end(), item);        if (it != rep.end()) rep.erase(it);
    }template<typename T>
    std::size_t Set<T>::size() const
    {return rep.size();
    }
    

请记住 :

  • public继承的意义完全不同.
  • 在应用域中, 复合意味着has-a. 在实现域, 复合意味着is-implemented-in-terms-of

条款39 : 明智而审慎地使用private继承

通过本条款, 你将明晰private继承在继承体系中充当了什么样的角色, 其与public继承和复合又有怎样的区别.

没错, private继承也有其所意味的东西, 但是这个意义我们已经了解过了, 那就是is-implemented-in-terms-of.

没错, private继承和复合除开在底层的实现细节不同, 它们可以实现相同的目的, 虽然有着不同的限制.

我们还是先回忆一下private继承会发生什么 :

  • 编译器不会自动将该派生类对象隐式转换为基类对象, 也就是说基类参数接口将无法接受该对象.
  • 所有从基类继承而来的属性在派生类中都是private的, 纵使它们原来是public/protect.
class Person { ...
public:void speak();    ...
};
class Student: private Person { ... };     // 如果是private继承Personvoid eat(const Person& p);				   // 按常理所有人都可以吃Person p;                                  // p is a Person
Student s;                                 // s is a Studenteat(p);                                    // 正确
eat(s);									   // 错误! 无法隐式转换为基类
cout << s.speak() << endl;				   // 错误! public已经转为private

我们可以看出private继承和is-a的关系完全不沾边, 最终的实际效果其实就是is-implemented-in-terms-of(根据某物实现出).

若B private继承 自A, 说明A需要采用B中备妥的某些特性, A不需要传递B的什么接口, 而是希望使用B的一部分功能实现, 可以是直接使用, 也可以是有目的的重写, 我们可以肯定的是这样至少要比我们单独实现一份需求的功能来得方便.


什么时候使用private继承

既然我们知道private继承和复合都可以实现is-implemented-in-terms-of的效果, 那么应该选哪个呢?

答案是如果可以选复合, 最好优先选择复合, 毕竟private继承有时十分晦涩, 会大大降低代码的可读性, 我们对private继承的使用应当是明智而审慎的.

接下我们将说明什么情况下可以选用private继承 :

  • 涉及protected成员/virtual函数时. 当我们希望继承并重写一些virtual函数或是使用一些protected成员时, 如果我们不希望它们将基类接口或成员暴露出去, 就可以采用private继承, 因为其有天然转换为private的属性.

    以下是书中的一个例子, Widget想利用Timer中的计时机制来实现一些定时触发的机制, 它希望重写其中的onTick() :

    class Timer {
    public:explicit Timer(int tickFrequency);virtual void onTick() const;     // Timer每过一段时间就会触发一次onTick()...
    };class Widget: private Timer {		// private继承
    private:virtual void onTick() const;      // 重写Timer中的onTick(), 使其可以定时查看Widget的数据......
    };
    

    这样用户就不可能在外部调用到任何的onTick函数(包括基类和派生类的), 毕竟这只是为了实现内部的功能.

  • 有极大的空间要求时. 我们有时会面对一些空间十分有限的情况, 我们会非常希望去节省空间, 那么有一项技术值得我们研究 :

    先引入一些前提, 我们有些时候会有创建并使用一些空类的需求, 目的在于用作类型标识, 占位符, 空对象之类的需求, 这样的需求始终存在并且在现代C++的重要性逐步提升, 比如通过类型表示进行类型推导, 实现在编译期即可进行判断的技术.

    // 类型标识空类
    class TypeA {};
    class TypeB {};//通过 if constexpr 在编译时确定类型并执行不同的行为, 提高了运行期效率
    template <typename T>
    void identifyType(T obj) {if constexpr (std::is_same_v<T, TypeA>) {   // is_same_v是C++17引入的判断类型是否相同的类型特征std::cout << "TypeA\n";} else if constexpr (std::is_same_v<T, TypeB>) {std::cout << "TypeB\n";} else {std::cout << "Unknown type\n";}
    }int main() {identifyType(TypeA{});  // 输出 "TypeA"identifyType(TypeB{});  // 输出 "TypeB"
    }
    

    而使用这些空类符合"is-implemented-in-terms-of"的意味, 假设我们就是为了做类型标识, 那么如果我们想赋予一个类对应的类型标识来达到分类的效果, 我们可以让这个类复合或private继承空类来实现, 在这种情况下我们建议优先选择private继承, 这样可以通过继承的类型在编译器做出更多的操作, 并且也减小了对象大小, 这就是所谓的EBO(空白基类最优化).

    简单来说就是复合会增加对象大小(就算是空类也会), 而private继承完全不会增加大小, 而且还给了我们在编译期的可操作空间, 我们可以做到以下编译期类型识别的效果 :

    // 空类型标识基类
    class TypeA {};
    class TypeB {};// 派生类,通过private继承不同的空基类来在编译期区分类型
    class MyClass : private TypeA {};
    class AnotherClass : private TypeB {};//通过 if constexpr 在编译时确定类型并执行不同的行为
    template <typename T>
    void identifyType(T obj) {if constexpr (std::is_base_of_v<TypeA, T>) {   // is_base_of_v是C++17引入的判断基类类型的类型特征std::cout << "Object is of TypeA\n";} else if constexpr (std::is_base_of_v<TypeB, T>) {std::cout << "Object is of TypeB\n";} else {std::cout << "Unknown type\n";}
    }int main() {MyClass a;AnotherClass b;identifyType(a);  // 输出: Object is of TypeAidentifyType(b);  // 输出: Object is of TypeB
    }
    

复合的优势

在上文我们讲了两种建议使用private继承的情况, 那么除此之外我们还是推荐能使用复合就使用复合, 其优势在于 :

  • 可读性高.
  • 使用复合对象可以防止派生类重写其virtual函数. (private继承不可, 因为就算是private继承也可以重写private的virtual函数, 然后通过基类指针调用, 见条款35)
  • 使用复合就可以使编译依存性降至最低, 如果继承必须可见定义, 而复合只需声明即可. (详见条款31)

"public继承 + 复合"替代private继承

我们还可以通过"public继承 + 复合"替代一些场景下private继承的作用, 虽然这样会麻烦一些, 但值得我们考量, 我们将上文的Timer案例重写一下 :

class Widget {
private:class WidgetTimer: public Timer {public:virtual void onTick() const;...};WidgetTimer timer;...
};

这个例子中我们创建一个WidgetTimer的内部类, 其public继承自Timer, 其重写了onTick(), 并且我们立马在下面创建一个对应的复用对象, 我们可以发现这个private继承的效果类似, 并且也可以体现上述复合的优势.

当然这只适用于部分情况, private继承肯定还是有用武之地的.


请记住 :

  • private继承意味着is-implemented-in-terms-of.
  • 任何时候都优先选择复合, 除非一些特殊情况下.
  • private继承可以支持EBO, 并且在C++17中可以继承类型标识空类来实现编译期逻辑判断.

by 天目中云

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

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

相关文章

重温设计模式--中介者模式

中介者模式介绍 定义&#xff1a;中介者模式是一种行为设计模式&#xff0c;它通过引入一个中介者对象来封装一系列对象之间的交互。中介者使得各个对象之间不需要显式地相互引用&#xff0c;从而降低了它们之间的耦合度&#xff0c;并且可以更方便地对它们的交互进行管理和协调…

【开源库 | xlsxio】C/C++读写.xlsx文件,xlsxio 在 Linux(Ubuntu18.04)的编译、交叉编译

&#x1f601;博客主页&#x1f601;&#xff1a;&#x1f680;https://blog.csdn.net/wkd_007&#x1f680; &#x1f911;博客内容&#x1f911;&#xff1a;&#x1f36d;嵌入式开发、Linux、C语言、C、数据结构、音视频&#x1f36d; ⏰发布时间⏰&#xff1a; 2024-12-20 …

NACA四位数字翼型

NACA四位数字翼型&#xff0c;以NACA 2412为例 第一位数字2 —相对弯度 第二位数字4 —相对弯度所有位置&#xff08;单位化后的&#xff09; 最末两位数字12 —相对厚度 所有NACA四位数字翼型的&#xff08;相对厚度所在的位置&#xff09;

DataX与DataX-Web安装与使用

DataX github地址&#xff1a;DataX/introduction.md at master alibaba/DataX GitHub 环境准备 Linux环境系统 JDK&#xff08;1.8及其以上版本&#xff0c;推荐1.8&#xff09; Python&#xff08;2或者3都可以&#xff09; Apache Maven 3.x&#xff08;源码编译安装…

电子应用设计方案69:智能护眼台灯系统设计

智能护眼台灯系统设计 一、引言 随着人们对眼睛健康的重视&#xff0c;智能护眼台灯成为了越来越多人的选择。本设计方案旨在打造一款功能丰富、护眼效果显著且智能便捷的台灯系统。 二、系统概述 1. 系统目标 - 提供无频闪、无蓝光危害的均匀柔和光线&#xff0c;保护眼睛。…

cesium 常见的 entity 列表

Cesium 是一个用于创建3D地球和地图的开源JavaScript库。它允许开发者在Web浏览器中展示地理空间数据,并且支持多种类型的空间实体(entities)。 Entities是Cesium中用于表示地面上或空中的对象的一种高层次、易于使用的接口。它们可以用来表示点、线、多边形、模型等,并且可…

在Visual Studio 2022中配置C++计算机视觉库Opencv

本文主要介绍下载OpenCV库以及在Visual Studio 2022中配置、编译C计算机视觉库OpenCv的方法 1.Opencv库安装 ​ 首先&#xff0c;我们需要安装OpenCV库&#xff0c;作为一个开源库&#xff0c;我们可以直接在其官网下载Releases - OpenCV&#xff0c;如果官网下载过慢&#x…

【Java基础面试题035】什么是Java泛型的上下界限定符?

回答重点 Java泛型的上下界限定符用于对泛型类型参数进行范围限制&#xff0c;主要有上界限定符和下届限定符。 1&#xff09;上界限定符 (? extends T)&#xff1a; 定义&#xff1a;通配符?的类型必须是T或者T的子类&#xff0c;保证集合元素一定是T或者T的子类作用&…

WPF+MVVM案例实战与特效(四十七)-实现一个路径绘图的自定义按钮控件

文章目录 1、案例效果2、创建自定义 PathButton 控件1、定义 PathButton 类2、设计样式与控件模板3、代码解释3、控件使用4、直接在 XAML 中绑定命令3、源代码获取4、总结1、案例效果 2、创建自定义 PathButton 控件 1、定义 PathButton 类 首先,我们需要创建一个新的类 Pat…

共模电感的工作原理

共模电感也称为共模扼流线圈&#xff0c;是一种抑制共模干扰的器件&#xff0c;它是由两个尺寸相同&#xff0c;匝数相同的线圈对称地绕制在同一个铁氧体环形磁芯上&#xff0c;形成的一个四端器件。当共模电流流过共模电感时&#xff0c;磁芯上的两个线圈产生的磁通相互叠加&a…

外连接转AntiJoin的应用场景与限制条件 | OceanBase SQL 查询改写系列

在《SQL 改写系列&#xff1a;外连接转内连接的常见场景与错误》一文中&#xff0c;我们了解到谓词条件可以过滤掉连接结果中的 null 情形的&#xff0c;将外连接转化为内连接的做法是可行的&#xff0c;正如图1中路径(a)所示。此时&#xff0c;敏锐的你或许会进一步思考&#…

二、windows环境下vscode使用wsl教程

本篇文件介绍了在windows系统使用vscode如何连接使用wsl&#xff0c;方便wsl在vscode进行开发。 1、插件安装 双击桌面vscode&#xff0c;按快捷键CtrlShiftX打开插件市场&#xff0c;搜索【WSL】点击安装即可。 2、开启WSL的linux子系统 点击左下方图标【Open a Remote Win…

因子问题(真EASY)

描述 任给两个正整数N、M&#xff0c;求一个最小的正整数a&#xff0c;使得a和(M-a)都是N的因子。 输入描述 包括两个整数N、M。N不超过1,000,000。 输出描述 输出一个整数a&#xff0c;表示结果。如果某个案例中满足条件的正整数不存在&#xff0c;则在对应行输出-1 用例…

2024 高频 Java 面试合集整理 (1000 道附答案解析)

2024 年马上就快要过去了&#xff0c;总结了上半年各类 Java 面试题&#xff0c;初中级和中高级都有&#xff0c;包括 Java 基础&#xff0c;JVM 知识面试题库&#xff0c;开源框架面试题库&#xff0c;操作系统面试题库&#xff0c;多线程面试题库&#xff0c;Tcp 面试题库&am…

(2024.12)Ubuntu20.04安装openMVS<成功>.colmap<成功>和openMVG<失败>记录

一、安装openMVS 官方文档&#xff1a;https://github.com/cdcseacave/openMVS/wiki/Building sudo apt-get -y install git mercurial cmake libpng-dev libjpeg-dev libtiff-dev libglu1-mesa-dev eigen git clone https://gitlab.com/libeigen/eigen --branch 3.4 mkdi…

自动控制系统综合与LabVIEW实现

自动控制系统综合是为了优化系统性能&#xff0c;确保其可靠性、稳定性和灵活性。常用方法包括动态性能优化、稳态误差分析、鲁棒性设计等。结合LabVIEW&#xff0c;可以通过图形化编程、高效数据采集与处理来实现系统综合。本文将阐述具体方法&#xff0c;并结合硬件选型提供实…

【恶意软件检测】一种基于API语义提取的Android恶意软件检测方法(期刊等级:CCF-B、Q2)

一种基于API语义提取的Android恶意软件检测方法 A novel Android malware detection method with API semantics extraction 摘要 由于Android框架和恶意软件的持续演变&#xff0c;使用过时应用程序训练的传统恶意软件检测方法在有效识别复杂演化的恶意软件方面已显不足。为…

FLTK - build fltk-1.1.10 on vs2019

文章目录 FLTK - build fltk-1.1.10 on vs2019概述笔记buildtest测试程序运行 END FLTK - build fltk-1.1.10 on vs2019 概述 看书上用到了fltk-1.1.10, 用vs2019试试能否正常编译使用? 笔记 build 从官网下载fltk-1.1.10-source.tar.bz2 用7zip解开 fltk-1.1.10-source.…

业财融合,决策有据:工程项目管理的财务新视角

在工程项目管理领域&#xff0c;业财融合正开启全新篇章。传统模式下&#xff0c;业务与财务各自为政&#xff0c;常导致信息滞后、决策盲目。如今&#xff0c;借助先进理念与技术&#xff0c;二者紧密相连。 在项目规划阶段&#xff0c;财务部门依据业务需求与市场趋势&#…

汽车IVI中控开发入门及进阶(44):杰发科智能座舱芯片

概述: 杰发科技自成立以来,一直专注于汽车电子芯片及相关系统的研发与设计。 产品布局: 合作伙伴: 杰发科技不断提升产品设计能力和产品工艺,确保产品达 到更高的质量标准。目前杰发科技已通过ISO9001质 量管理体系与CMMIL3认证。 杰发科技长期合作的供应商(芯片代工厂、…