Effective C++条款40:明智而审慎地使用多重继承(Use multiple inheritance judiciously)

news2024/11/27 12:53:32

Effective C++条款40:明智而审慎地使用多重继承(Use multiple inheritance judiciously)

  • 条款40:明智而审慎地使用多重继承
    • 1、多重继承的两个阵营
    • 2、多重继承中,接口调用的歧义性
    • 3、菱形继承与虚(virtual)继承
      • 3.1 菱形继承中数据成员的重复问题
      • 3.2 C++如何处理菱形继承
    • 4、虚继承的代价
    • 5、多重继承案例
    • 6、牢记
  • 总结


《Effective C++》是一本轻薄短小的高密度的“专家经验积累”。本系列就是对Effective C++进行通读:

第6章:继承与面向对象设计

在这里插入图片描述


条款40:明智而审慎地使用多重继承

1、多重继承的两个阵营

  当涉及到多重继承(MI)的时候,C++社群便被分为两个基本阵营。其一是:认为如果单一继承(SI)是好的,多重继承肯定会更好。另一个则主张:单一继承是好的,但多重继承不值得拥有(或使用)。本条款中,我的主要目标是让你明白多重继承的这两个观点。

2、多重继承中,接口调用的歧义性

  当一个类继承自两个基类时,两个基类包含有相同的名称(如函数、typedef等),那么调用时就会产生歧义性

class BorrowableItem {   // 图书馆允许你借某些东西
public:                    
	void checkOut();    // 离开进行检查
	 ...                                       
};                                       
class ElectronicGadget {    
private:                             
	bool checkOut() const;  //注意,此处的为private 
	... 
};
//多重继承
class MP3Player:
	public BorrowableItem,
	public ElectronicGadget 
{ ... };              
MP3Player mp;
mp.checkOut();          //歧义!调用的是哪个checkOut

  注意此例子中,对checkout的调用是歧义的,即使只有两个函数中的一个是可取用的。(checkout在BorrowableItem中是public的而在ElectronicGadget中是private的)。这与C++用来解析(resolving)重载函数调用的规则相符:在看到是否有个函数可取用之前,C++首先首先识别出函数调用的最佳匹配函数。找到最佳匹配函数之后才会检查函数的可取用性。

  本例中两个checkout有相同的匹配程度,所以既不能解决函数重载的问题也不能决定最佳匹配函数。ElectronicGadget::checkOut的可访问性根本不会被检查到。

  为了解决这个歧义,你必须指定调用哪个基类的函数:

mp.BorrowableItem::checkOut();   

  当然你也可以显示调用ElectronicGadget::checkOut,但是你会获得一个“尝试调用private成员函数”的错误。

3、菱形继承与虚(virtual)继承

3.1 菱形继承中数据成员的重复问题

  多重继承仅仅意味着从多个基类(多于一个)中继承,但是对于多重继承来说在继承体系中发现更高层次的基类也并不是不常见。这就导致了我们常说的致命的“钻石型多重继承”:

class File { ... };
class InputFile: public File { ... };
class OutputFile: public File { ... };
class IOFile: public InputFile, public OutputFile
{ ... };

在这里插入图片描述

  任何时候在一个继承体系中,如果基类和派生类之间有一条以上的相通路线,你必须面对基类中的数据成员是否在每条路径上都要被复制的问题。假设File类有一个数据成员,fileName。IOFile应该有它的几份拷贝?从一方面讲,它从每个基类中都继承了一份拷贝,所以表明IOFile应该会有两个fileName数据成员。从另外一方面讲,一个IOFIle只有一个文件名,所以从两个基类中继承的fileName部分不应该被重复。

3.2 C++如何处理菱形继承

  C++在这场辩论中没有倾斜立场。两个方案它都支持——虽然其缺省做法是执行复制(也就是上一段所说的第一个做法)。如果那不是你想要的,你必须将包含数据(也即是File)的类变为虚基类。为了达到这个目的,你会对所有继承自它的类使用虚继承。

class File { ... };
class InputFile: virtual public File { ... };
class OutputFile: virtual public File { ... };
class IOFile: public InputFile, public OutputFile
{ ... };

在这里插入图片描述

  C++标准库程序内含一个多重继承体系,如上图,但是类模版的类不在其中,这些类的名字是basic_ios,basic_istream,basic_ostream和basic_iostream,它们分别替换了File,InputFile,OutputFile和IOFile。

4、虚继承的代价

  从正确行为的观点看,public继承应该总是virtual的。如果你只是唯一一个观点,规则会很简单:在你使用public继承的任何时候都要使用virtual public继承。但正确性不是我们要唯一关注的,为了阻止对继承而来的字段进行重复,编译器会在背后耍一些花招,结果是使用虚继承的类创建出来的对象会比不使用虚继承的类创建出来的对象要大。访问虚基类中的数据成员比访问非虚基类中的数据成员要慢。详细情况随编译器的不同而不同,但是基本重点也很清楚:虚继承耗费资源

  虚继承的成本还包括其他方面。支配虚基类初始化列表的规则比非虚基类更加复杂,且不直观。初始化虚基类部分的责任由继承体系中最底层的派生类承担。这种规则就意味着:

  • ① 继承自虚基类的类如果需要初始化,它们必须意识到虚基类的存在,无论这个虚基类离派生类有多远。

  • ② 当一个派生类被添加到继承体系中的时候,它必须承担初始化虚基类的责任(无论是直接的还是间接的虚基类)。

  我对于使用虚基类(也就是虚继承)的建议很简单。

  • ① 不要使用虚基类,除非你需要它。默认情况下使用非虚基类。

  • ② 如果你必须使用虚基类,尝试着不要在这些类中放置数据。这样你就不必为这些类的初始化(还有赋值)规则的古怪行为进行担心了。值得注意的是,Java和.NET中的接口(在许多方面相当于C++的虚基类)是不允许包含任何数据的。

5、多重继承案例

  让我们看一下下面的C++接口类(见条款31)

IPerson类

  下面是一个抽象类:其中包含纯虚函数name()和birthDate()

class IPerson {
public:
	virtual ~IPerson();
	virtual std::string name() const = 0;  //返回人的名称
	virtual std::string birthDate() const = 0;  //返回生日
};

  IPerson的客户必须依赖IPerson指针和引用来进行编程,因为抽象类不能被实例化。为了创建可以被IPerson对象操作的对象,IPerson的客户使用工厂函数(条款31)来实例化派生自Person的具现类:

std::tr1::shared_ptr<IPerson> makePerson(DatabaseID personIdentifier);
DatabaseID askUserForDatabaseID();
DatabaseID id(askUserForDatabaseID());
std::tr1::shared_ptr<IPerson> pp(makePerson(id)); // create an object
...                                                                            

  但是如何使用makePerson创建返回指针指向的对象呢?无疑地一定有些派生自IPerson具现类,使得makePerson能够对这些具现类进行实例化。

  我们把这个类叫做CPerson。就像具现类一样,CPerson必须为继承自IPerson的纯虚函数提供一份实现代码。我们可以从头开始实现这个函数,但是利用现成的组件来对其进行实现更好,这些现成的组件实现了大部分或者全部的必要功能。例如,假设一个旧数据库指定的类PersonInfo为CPerson提供了它需要的最基本的东西:

PersonInfo类

class PersonInfo {
public:
	explicit PersonInfo(DatabaseID pid);
	virtual ~PersonInfo();
	virtual const char * theName() const;
	virtual const char * theBirthDate() const;
	...
private:
	virtual const char * valueDelimOpen() const; 
	virtual const char * valueDelimClose() const; 
	...
};

valueDelimOpen()、valueDelimClose():

  • 功能:每个字段值的起点和结尾都以特殊字符串为界

  • 缺省的头尾界限符号是方括号。例如Ring-tailed Lemur将被格式化为:[Ring-tailed Lemur]

  • 每个人可能喜欢不同的界限符号,所以这两个virtual函数允许派生类自己定义不同的头尾界限符号。例如可能PersonInfo的派生类可能会重写这两个虚函数,代码如下:

//缺省的虚函数,派生类可以重写
const char* valueDelimOpen()const
{
    return "[";
}
 
const char* valueDelimClose()const
{
    return "]";
}

theName()、theBirthDate():用来返回相关的数据库字段(名字、生日等)。下面以theName()为例:

const char* theName()const {
    static char value[Max_Formatted_Field_Value_Length];
    std::strcpy(value, valueDelimOpen());
 
    //将名字添加进value
    
    std::strcat(value, valueDelimClose());
    return value;
}

  作为CPerson的实现者,这是个好消息,因为当对IPerson的文档进行精读时,你发现name和birthDate需要返回没有分隔符的值。也就是一个叫做”Homer”的人,对这个名字进行函数调用会返回“Homer”而不是“[Homer]”。

  CPerson和PersonInfo之间的关系是PersonInfo恰好有一些函数使得CPerson的实现更加容易。它们的关系因此为“is-implemented-in-terms-of”,我们知道这种关系可以被表现为其它两种形式:通过组合(条款38)和private继承(条款39)。条款39指出组合通常讲比Private继承要更好,但如果虚函数需要重定义,private继承就是必须的。在这种情况中,CPerson需要重新定义valueDelimOpen和valueDelimClose,所以使用组合在这里不能工作。最简单直接的解决方案是让CPerson private继承PersonInfo,虽然Item 39解释道如果多做一些工作,CPerson可以使用组合和继承的结合体来有效重定义PersonInfo的虚函数。在这里,我们使用private继承。

  但是CPerson同样必须实现IPerson接口,这些接口为public继承所用。这也导致了合理的多重继承应用:将一个接口的public接口和一个实现的private继承结合起来使用:

CPerson类
CPerson是最终的表示“人”的类,其继承于IPerson和PersonInfo

公有继承于IPerson:

  • 因为IPerson的name()和birthDate()两个虚函数返回未经修饰的人物的名称和生日,并且IPerson为抽象类,因此CPerson以public继承于IPerson

私有继承于PersonInfo:

  • PersonInfo已经提供了返回修饰的人名和生日的虚函数,因此CPerson可以利用PersonInfo来实现,这是一种is-implemented-in-terms-of(根据某物实现出)模式
class IPerson {
public:
    virtual ~IPerson();
    virtual std::string name()const = 0;
    virtual std::string birthDate()const = 0;
};
 
class DatabaseID {};
 
class PersonInfo {
public:
    explicit PersonInfo(DatabaseID pid);
    virtual ~PersonInfo();
    virtual const char* theName()const;
    virtual const char* theBirthDate()const;
private:
    virtual const char* valueDelimOpen()const;
    virtual const char* valueDelimClose()const;
};
 
class CPerson :public IPerson, private PersonInfo {
public:
    explicit CPerson(DatabaseID pid) :PersonInfo(pid) {}
 
    virtual std::string name()const = 0{
        return PersonInfo::theName();
    }
    virtual std::string birthDate()const = 0 {
        return PersonInfo::theBirthDate();
    }
private:
    virtual const char* valueDelimOpen()const;
    virtual const char* valueDelimClose()const;
};                                                               

在UML图中这个设计看起来像这样:
在这里插入图片描述

  这个例子告诉我们,多重继承也有它的合理用途的。

6、牢记

  • 多重继承比单一继承复杂。它可可能导致新的歧义性,以及对virtual继承的需要。

  • virtual继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果virtual base classes不带任何数据,将是最具使用价值的情况。

  • 多重继承的确有正当用途。其中一个情节涉及“public继承某个Interface class”和“private继承某个协助实现的class”的两项组合。

总结

期待大家和我交流,留言或者私信,一起学习,一起进步!

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

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

相关文章

注意力FM模型AFM

1. 概述 在CTR预估任务中&#xff0c;对模型特征的探索是一个重要的分支方向&#xff0c;尤其是特征的交叉&#xff0c;从早起的线性模型Logistic Regression开始&#xff0c;研究者在其中加入了人工的交叉特征&#xff0c;对最终的预估效果起到了正向的效果&#xff0c;但是人…

华为MPLS跨域C1方案实验配置

目录 配置接域内IGP路由协议与LDP协议 配置IPv4的BGP邻居 配置PE之间的Vpnv4邻居 配置PE与CE设备对接命令 ASBR上手工为PE地址分配标签 MPLS隧道——跨域解决方案C1、C2讲解_静下心来敲木鱼的博客-CSDN博客_route-policy rr permit node 10 if-match mpls-labelhttps://bl…

IB地理课选课指南,SL还是HL适合呢?

IB地理科的标准级别&#xff08;Standard Level&#xff0c; SL&#xff09;课程跟高级级别&#xff08;Higher Level&#xff0c;HL&#xff09;课程的最大不同处在于&#xff0c;考卷的数量跟题目的数量是不同的。可是&#xff0c;两者之间的教学内容和科目指引&#xff08;S…

二十八、Kubernetes中job详解

1、概述 在kubernetes中&#xff0c;有很多类型的pod控制器&#xff0c;每种都有自己的适合的场景&#xff0c;常见的有下面这些&#xff1a; ReplicationController&#xff1a;比较原始的pod控制器&#xff0c;已经被废弃&#xff0c;由ReplicaSet替代 ReplicaSet&#xff…

CentOS 7 升级安装 Python 3.9 版本

由于 yum install python3 默认安装的 Python 版本较低&#xff0c;现如今有更高版本的 Python 需求&#xff0c;就想用编译安装的方法安装一个较高版本的 Python&#xff0c;顺道记录一下安装过程。 注意&#xff1a;不要卸载自带的 python2&#xff0c;由于 yum 指令需要 pyt…

idea中代码git的版本穿梭Git Rest三种模式详解(soft,mixed,hard)

使用Git进行版本控制开发时难免会遇到回顾的情况&#xff0c;这里来解释下该如何正确的回滚 文章目录1.本地仓库回滚2.远程仓库回滚2.1错误案例2.2正确操作3.代码提交到错误的分支解决4.Git Rest三种模式详解&#xff08;soft,mixed,hard&#xff09;4.1操作演示reset --hard&a…

【论文简述】FlowFormer:A Transformer Architecture for Optical Flow(ECCV 2022)

一、论文简述 1. 第一作者&#xff1a;Zhaoyang Huang、Xiaoyu Shi 2. 发表年份&#xff1a;2022 3. 发表期刊&#xff1a;ECCV 4. 关键词&#xff1a;光流、代价体、Transformer、GRU 5. 探索动机&#xff1a;现有的方法对代价体的信息利用有限。 6. 工作目标&#xff1…

RabbitMQ 部署及配置详解(集群部署)

RabbitMQ 集群是一个或 多个节点&#xff0c;每个节点共享用户、虚拟主机、 队列、交换、绑定、运行时参数和其他分布式状态。一、RabbitMQ 集群可以通过多种方式形成&#xff1a;通过在配置文件中列出群集节点以声明方式以声明方式使用基于 DNS 的发现以声明方式使用 AWS &…

Java中的LinkedList

文章目录前言一、LinkedList的使用1.1 什么是LinkedList1.2 LinkedList的使用1.2.1 LinkedList的构造1.2.2 LinkedList的其他常用方法介绍1.2.3 LinkedList的遍历二、LinkedList的模拟实现三、ArrayList和LinkedList的区别总结前言 上一节中我们讲解了Java中的链表&#xff0c…

vue3.0中echarts实现中图地图的省份切换,并解决多次切换后地图卡死的情况

一、echarts安装及地图的准备 1、安装echarts npm install echarts2、下载china.js等json文件到项目中的文件夹 map的下载地址&#xff1a; 等审核 二、代码说明 <template><div class"center-body"><div class"map" id"map"…

fork函数详解

文章目录fork函数例子详解工作原理GDB 多进程调试fork函数 fork系统调用用于创建一个新进程&#xff0c;称为子进程&#xff0c;它与进程&#xff08;称为系统调用fork的进程&#xff09;同时运行&#xff0c;此进程称为父进程。创建新的子进程后&#xff0c;两个进程将执行fo…

jvm系列(2)--类加载子系统

目录第2章-类加载子系统内存结构概述简图详细图类加载器子系统类加载器ClassLoader角色类加载过程概述加载阶段链接阶段验证(Verify)准备(Prepare)解析(Resolve)初始化阶段类的初始化时机clinit()1&#xff0c;2&#xff0c;3说明4说明5说明6说明类加载器的分类概述虚拟机自带的…

【web安全】——文件上传的绕过方式

作者名&#xff1a;白昼安全主页面链接&#xff1a; 主页传送门创作初心&#xff1a; 舞台再大&#xff0c;你不上台&#xff0c;永远是观众&#xff0c;没人会关心你努不努力&#xff0c;摔的痛不痛&#xff0c;他们只会看你最后站在什么位置&#xff0c;然后羡慕或鄙夷座右铭…

价值创造链路及经营计划

“价值创造过程最主要的环节是建立链接&#xff0c;北京万柳书院在网上热议&#xff0c;其背后是人与人的大量链接&#xff0c;近期热议的湖南卫视春晚亦如是&#xff0c;这种链接为价值的设计、沟通、传递创造条件&#xff1b;企业以客户为中心设计产品&#xff0c;往大了说是…

C++ string类的初步了解

目录 一. 为什么学习string类&#xff1f; 1.C语言中的字符串 2.string类 二. string类的常用接口说明 1.构造 2.容量 size和length capacity clear empty reserve resize 3.元素访问 operator[] at front、back 4.迭代器 ​编辑begin、end rbegin、rend …

数据结构初阶:排序

本期博客我们来到了初阶数据结构最后一个知识点&#xff1a;排序 排序&#xff0c;我们从小到大就一直在接触&#xff0c;按身高、成绩、学号等等不同的排序我们已经历许多&#xff0c;那么各位是按怎样的方法进行排序的呢&#xff1f; 废话不多说这期博客我们对各种排序方法…

测试开发 | 测试平台开发-前端开发之数据展示与分析

本文节选自霍格沃兹测试学院内部教材测试平台的数据展示与分析&#xff0c;我们主要使用开源工具ECharts来进行数据的展示与分析。ECharts简介与安装ECharts是一款基于JavaScript的数据可视化图表库&#xff0c;提供直观&#xff0c;生动&#xff0c;可交互&#xff0c;可个性化…

Unity 使用OpenXR和XR Interaction Toolkit 开发 HTCVive(Vive Cosmos)

Unity 使用OpenXR和XR Interaction Toolkit 开发 HTCVive&#xff08;Vive Cosmos&#xff09; 提示&#xff1a;作者是 Unity 2020.3 以上版本做的开发。开发VR程序需要安装 Steam&#xff0c;SteamVR, (Vive Cosmos,需要再安装VIVEPORT,VIVEConsole) OpenXR 控制设备 &#x…

OpenCV(12)-OpenCV的机器学习

OpenCV的机器学习 基本概念 计算机视觉是机器学习的一种应用&#xff0c;而且是最有价的应用 人脸识别 哈尔(Haar)级联方法深度学习方法(DNN) Haar人脸识别方法 哈尔(Haar)级联方法是专门为解决人脸识别而推出的&#xff0c;在深度学习还不流行时&#xff0c;哈尔已可以商…

Android 深入系统完全讲解(21)

关键性 EGLSurface 代码位置 继续再看看&#xff0c;代码跑到 C 里面去了。 然后关键点&#xff1a; 获取本地窗口&#xff0c;创建 Surface&#xff0c;然后 toEGLHandle 进行包裹&#xff0c;变成 EGL 上下文。 EGLSurface 。 绘制的设计本质逻辑 在这里就回归一点&#xff…