Effective C++条款27:尽量少做转型动作(Minimize casting)

news2024/11/14 20:49:04

Effective C++条款27:尽量少做转型动作(Minimize casting)

  • 条款27:尽量少做转型动作
    • 1、数据类型转型语法回顾
      • 1.1 C风格的cast
      • 1.2 C++风格的cast
      • 1.3 新风格转型更受欢迎
    • 2、使用cast会产生运行时代码——不要认为你以为的就是你以为的
    • 3、转型的误用
    • 4、关于dynamic_cast的一些概述
      • 4.1 Dynamic_cast的两种替代方案
    • 5、牢记
  • 总结


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

第5章:实现

在这里插入图片描述


条款27:尽量少做转型动作

  C++设计的目标之一是,保证“类型错误”绝不可能发生。理论上来说,如果你的程序能够很干净的通过编译,它就不会尝试在任何对象上执行任何不安全或无意义的操作。这个保证很有价值,不要轻易放弃它。

  不幸的是,转型(casts)颠覆了类型系统。它导致了各种麻烦的出现,一些很容易识别,一些却很狡猾(不容易被识别)。如果你以前使用过C,java或者C#,请特别注意,因为在这些语言中casting是更加必不可少的,但却比C++更安全。C++不是C,不是java也不是C#,在C++中,你需要怀着极大的敬意来使用casting。

1、数据类型转型语法回顾

1.1 C风格的cast

  先让我们回顾一下casting的语法,通常有三种不同的方法来实现同一个cast。C风格的casts如下:

(T) expression // 将expression转型为T

  函数风格的casts使用下面的语法:

T(expression) // 将expression转型为T

  上面的两种形式在意义上没有区别,只是放括号的地方不一样。我们将这两种形式的casts叫做旧式风格的casts。

1.2 C++风格的cast

  C++同样提供四种新风格的casts形式(C++风格的casts):

const_cast<T>(expression)
dynamic_cast<T>(expression)
reinterpret_cast<T>(expression)
static_cast<T>(expression)

  各有不同目的:

  • Const_cast是用来去除对象的常量性的(constness)。在四个C++风格的cast中,const_cast是唯一能做到这一点的。

  • Dynamic_cast主要用来执行“安全的向下转型”,也就是决定一个特定类型的对象是否在一个继承体系中。这也是唯一一个不能用旧式风格语法来实现的cast。它也是唯一一个可能会出现巨大的运行时开销的cast。(稍后细谈解)

  • Reinterpret_cast被用来做低级的转型,结果可能取决于编译器,也就是代码不能被移植,例如,将一个指针转换成int。这种casts除了用在低级代码中,其他地方很少见。本书中只出现过一次,就是在讨论如何为原生内存(raw memory)实现一个调试分配器(条款50)。

  • Static_cast能被用来做强制的显示类型转换(比如,non-const对象转换成const对象(条款3),int转换成double等等。)它同样能够用来对这些转换进行反转(比如,void*转换成具体类型指针,指向base的指针转换成指向派生类的指针),但是不能从const转换成非const对象(只有const_cast能这么做)。

1.3 新风格转型更受欢迎

  旧式风格的转型仍然合法,但是新风格的更受欢迎。第一,在代码中它们更加容易被辨别(对于人或者工具来说),因此简化了在代码中寻找转型动作的过程。第二,每个cast更加特别的使用用途使得编译器能够诊断出使用错误成为可能。譬如,如果你使用其它3个cast而不是const_cast来去除常量的常量性,你的代码无法通过编译。

  我使用旧式风格转型的唯一地方是当我想通过调用一个explicit构造函数将一个对象传递给一个函数时。例如:

class Widget {
public:
	explicit Widget(int size);
	...
};
void doSomeWork(const Widget& w);
doSomeWork(Widget(15));                     // 以一个int加上“函数风格”的转型动作创建一个Widget 
doSomeWork(static_cast<Widget>(15));      // 以一个int加上C++风格的转型动作创建一个Widget 

  从某个角度来说,这种对象的创建不像是一个cast,所以使用了函数风格的cast而不是static_cast。(这两种方法做了相同的事情:创建一个临时Widget对象然后传递给doSomeWork。)需要再说一遍,使用旧式转型实现的代码往往当时感觉很合理,但日后可能出现core dump,所以最好忽略这种感觉,总是使用新型转型。

2、使用cast会产生运行时代码——不要认为你以为的就是你以为的

  许多程序员认为转型除了告诉编译器需要将一个类型当作另外一个类型之外,没有做任何事情,但这个一个误区。任何种类的类型转换(不管显示cast还是隐式转换(编译器完成))都会产生运行时代码。举个例子:

int x, y;
...
double d = static_cast<double>(x)/y; // x 除以y,使用浮点数除法
// floating point division

  将int x转换成double肯定会产生代码,因为在大部分计算机体系结构中,int的底层表述不同于double的底层表述。这也许不会让你吃惊,但下面的例子可能需要你多多注意了:

class Base { ... };
class Derived: public Base { ... };
Derived d;
Base *pb = &d;                                    // implicitly convert Derived* ⇒ Base*

  这里我们只是创建了一个指向派生类对象的基类指针,但有时候,这两个指针(Derived*和Base*)值将会不一样。在上面的情况中,运行时会在Derived*指针上应用一个偏移量来产生正确的Base*指针值。

  上个例子表明,单个对象(比如Derived类型的对象)可能有多于一个的地址(比如,当Base*指针指向这个对象和Derived*指向这个对象时有两个地址)。这在C,java和C#中不可能发生。事实上,当使用多继承时,这种情况总会发生,但在单继承中也能发生。这意味着在C++中你应该避免对一些东西是如何布局的做出假设。例如,将对象地址转换成char*指针然后在此指针上面进行指针算术运算几乎总是会产生未定义行为。

  偏移量“有时候“是需要的。对象的布局方式和地址被计算的方式会随编译器的不同而不同。这意味着仅仅因为你了解一种平台上的布局和转型并不意味着在别的平台上也能如此工作。

3、转型的误用

  关于cast的一件有趣的事情是容易写出看上去正确但实际错误的代码。比如,许多应用框架需要派生类中的虚函数实现首先要调用基类部分。假设我们有一个Window基类和一个SpecialWindow派生类,两个类中都定义了onResize虚函数。进一步假设SpecialWindow的onResize函数首先要调用Window的onResize函数。下面的实现方式看上去正确,实际上是错的:

class Window {                       // base class
public:                                   
	virtual void onResize() { ... }           // base onResize 实现代码
	...                                                   
};                                                   
class SpecialWindow: public Window {       // derived class
public:                                                       
	virtual void onResize() {                             // derived onResize 实现代码
	static_cast<Window>(*this).onResize();     // 将 *this 转为 Window,
	...	
	} 
...
};

为什么上面的类型转换是错误的:

  • 我们在函数中将*this转换为基类类型,然后尝试调用onResize()虚函数,但是该转型动作产生的实际上是一个“this对象之基类成分”的一个副本,然后在这个副本上调用onResize函数

  • 所以当在副本上面调用onResize()函数对于this对象根本没有任何的影响,于是客户端程序以为调用了onResize()函数使对象本身改变了,但是实际上没有改变,只是改变了一个临时对象而已

  替代方案:就是在虚函数中显式的调用基类的函数。例如将上面派生类的虚函数更改为下面的形式就是对的了

class SpecialWindow: public Window {
public:
	virtual void onResize() {
	Window::onResize(); // call Window::onResize
	... // on *this
	}
	...
};

  这个例子同样表明如果你发现你自己想使用cast了,它就标志着你可能会使用错误的方式来应用它。使用dynamic_cast的时候也是如此。

4、关于dynamic_cast的一些概述

  dynamic_cast主要用于:想要使用派生类的方法,但是此时只有一个基类的指针/引用,此时可以使用该类型转换将基类的指针转换为派生类指针。

为什么不建议使用该转型:因为该转换会使代码执行速度的非常慢

举个例子,至少有一种普通的实现在某种程度上是基于类名称的字符串比较。如果你正在一个4层深的单继承体系的对象上执行dynamic_cast,在这样一种实现(也就是上面说的普通实现)下每个dynamic_cast至多可能调用四次strcmp来比较类名称。一个层次更深的继承或者一个多继承可能开销会更大。这样实现是有原因的(它们必须支持动态链接(dynamic linking))。

4.1 Dynamic_cast的两种替代方案

  你需要dynamic_cast是因为你想在你坚信其是派生类对象之上执行派生类操作,但你只能通过基类指针或基类引用来操作此对象。有两种普通的方法避免使用dynamic_cast。

  • 第一,使用容器直接存储派生类对象指针(通常情况下使用智能指针,见条款13),这样就消除了通过基类接口来操纵这些对象的可能。

  举个例子,在我们的window/SpecialWindow继承体系中,只有SpecialWindows支持blink,不要像下面这样做:

class Window { ... };
 
class SpecialWindows: public Window {
public:
    void blink();
    ...
};
int main()
{
	typedef
    std::vector<std::tr1::shared_ptr<Window> > VPW;
	
    //遍历容器中的每个Window对象并且调用blink函数
    for (auto iter = VPW.begin(); iter != VPW.end(); ++iter) {
        //使用dynamic_cast转型,get()函数是获取shared_ptr中的Window对象指针的意思
        if (SpecialWindows* psw = dynamic_cast<SpecialWindows*>(iter->get())) {// 不希望使用dynamic_cast
            psw->blink();
        }
    }
	return 0;
}

替代方法① :直接使用容器存储派生类对象,不存储基类对象

int main()
{
	typedef
    std::vector<std::tr1::shared_ptr<SpecialWindows> > VPW;
	
    for (auto iter = VPW.begin(); iter != VPW.end(); ++iter) {
        (*iter)->blink();
    }
    return 0;
}

缺点:但是这种方法可能需要为每一种派生类都创建一个容器来存储

替代②:当你在基类中想做一些事情,那么也就基类中同时声明一份,并且将函数设置为virtual的(但是基类中的虚函数什么都不做)

class Window {
public:
    virtual void blink() { //条款34告诉你缺省实现代码可能是个馊主意
        //什么都不做
    }
};
 
class SpecialWindows :public Window {
public:
    virtual void blink() {
        //实现相关代码
    }
};
 
int main()
{
    std::vector<std::tr1::shared_ptr<Window> > VPW;
	
    for (auto iter = VPW.begin(); iter != VPW.end(); ++iter) {
        (*iter)->blink();
    }
    return 0;
}

  上面的两种方法不是在任何情况下都能使用,但是在许多情况下,它们为dynamic_cast提供了一种可行的替代方案。当他们确实能做到你想要的,你应该拥抱它们。

  绝对必须避免的一件事情就是所谓的“连串dynamic_cast”,例如下面的代码,下面的代码运行又大又慢,而且基础不稳定,因为每次Window类继承体系一旦改变,所有的代码都需要再次更改

class Window {};
class SpecialWindows :public Window {};
class SpecialWindows2 :public Window {};
class SpecialWindows3 :public Window {};
//...
 
int main()
{
    std::vector<std::tr1::shared_ptr<Window> > VPW;
 
    for (auto iter = VPW.begin(); iter != VPW.end(); ++iter)
    {
        if (SpecialWindows *psw1 = dynamic_cast<SpecialWindows*>(iter->get())) {
            //...
        }
        else if (SpecialWindows2 *psw2 = dynamic_cast<SpecialWindows2*>(iter->get())) {
            //...
        }
        else if (SpecialWindows3 *psw3 = dynamic_cast<SpecialWindows3*>(iter->get())) {
            //...
        }
        //...
    }
    return 0;
}

5、牢记

  • 如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_cast。如果有个设计需要转型动作,试着发展无需转型的替代设计。

  • 如果转型是必须的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需要将转型放进它们自己的代码。

  • 宁可使用新式的转型,不要使用旧式转型。前者很容易辨别出来,而且也比较有着分门别类的职掌。

总结

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

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

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

相关文章

“一人负债,全家背锅”,严厉打击信用卡套现欺诈

不可否认&#xff0c;负债消费往往与高风险密不可分。 近日&#xff0c;一则关于网购的新闻冲上热搜。故事的主人公沉迷于网购&#xff0c;工资不足便利用信用卡透支&#xff0c;长期的积累已至入不敷出&#xff0c;只能向家人求助&#xff0c;家人也因此欠下巨额债务。最终&a…

文本-图像生成(Text-to-Image Generation)的评价指标介绍——CLIPScore、TISE

目录CLIPScore: A Reference-free Evaluation Metric for Image Captioning背景公式总结TISE: Bag of Metrics for Text-to-Image Synthesis Evaluation背景文本-图像生成基本评价指标图像质量和多样性图像和文本相关性创新点1&#xff1a;IS*创新点2&#xff1a;多目标文本-图…

GIS基础测量、地形分析、位置分析、空间分析功能介绍与实操应用

通知 入门级、进阶级一、二、三期、高阶级一期已完成&#xff0c;大家可进入公众号“图新地球”查看底部菜单&#xff1a;2022教程&#xff0c;获得软件直播课程的相关资料&#xff0c;包括直播讲解、直播PPT、直播的示例数据。 另外&#xff0c;本周周三12月7日将举行进阶级…

2020全栈学习Demo大合集 AllDemo-996station GitHub鉴赏官

推荐理由&#xff1a; 2020全栈学习Demo大合集 包含最新 hooks TS 等 还有umidva,数据可视化等实战项目 (持续更新中) 全栈学习 Demo 大合集 说明: 本项目包含常用的技术点和技术栈,时间为 2020 年度最新的技术栈,大范围的包含(Vue,Vuex,SSR,vue 源码解析,vue 实战,vue 单元测…

使用php解压缩ZipArchive类实现后台管理升级的解决方案

项目说明 开发php项目管理系统&#xff0c;由于是新项目且已经部署在生产环境&#xff0c;导致需要根据实际使用情况&#xff0c;进行及时的功能升级或bug修复。 每次升级&#xff0c;进行程序打包&#xff0c;然后通过FTP上传覆盖&#xff1b;后期因服务器转为内网&#xff…

自然语言处理基本概念 natural_language_processing-996station GitHub鉴赏官

推荐理由&#xff1a;自然语言处理基本概念 自然语言处理技术 第1章 语言模型 第2章 隐马尔可夫模型 第3章 最大熵模型 第4章 条件随机场模型 适用人群&#xff1a;自然语言 推荐指数&#xff1a;3 项目名称&#xff1a;natural_language_processing 自然语言处理基本概念 …

二叉树的遍历

二叉树的遍历 文章目录二叉树的遍历•二叉树的遍历定义• 二叉树的三种遍历方式先序遍历的递归算法中序和后序遍历的递归算法二叉树遍历的相关问题♥问题1解题模型▪算法构思:▪代码实现:♥问题2解题模型构建思路:代码如下:♥问题3算法构思:代码构建:♥问题4算法思路:解题模型:…

mysql读写分离

MySQL读写分离 原理 读写分离基本的原理是让主数据库处理事务性增、改、删操作&#xff08;INSERT、UPDATE、DELETE&#xff09;&#xff0c;而从数据库处理SELECT查询操作。数据库复制被用来把事务性操作导致的变更同步到集群中的从数据库。 读写分离就是只在主服务器上写&…

自学前端到什么程度,可以去找工作呢?

前言 可以看看现在市面上关于前端工程师职业招聘的相关要求&#xff0c;从以下两个的招聘可以看出&#xff0c;无论是普通的前端构造工程师还是高级前端开发工程师&#xff0c;对于h5、css3、es6以及相关框架如vue、react等都需要有深入的认知并能熟练运用 基于上面的相关要求…

刷爆力扣之第三大的数

刷爆力扣之第三大的数 HELLO&#xff0c;各位看官大大好&#xff0c;我是阿呆 &#x1f648;&#x1f648;&#x1f648; 今天阿呆继续记录下力扣刷题过程&#xff0c;收录在专栏算法中 &#x1f61c;&#x1f61c;&#x1f61c; 该专栏按照不同类别标签进行刷题&#xff0c;每…

(四)Spring Security Oauth2.0 源码分析--客户端端鉴权(token校验)

一 引言 在上篇文章我们分析了token的获取过程,那么拿到token后,将token放在请求头中进行资源的访问,客户端是如如何对token进行解析的呢,本文带你走进token校验的源码解析,基本流程如下所示 客户端向资源服务器发起请求时,在请求头Authorization携带申请的token请求被Filte…

网络编程——BIO与NIO介绍与底层原理

BIO BIO(Blocking IO) 又称同步阻塞IO&#xff0c;一个客户端由一个线程来进行处理 当客户端建立连接后&#xff0c;服务端会开辟线程用来与客户端进行连接。以下两种情况会造成IO阻塞&#xff1a; 服务端会一直阻塞&#xff0c;直到和客户端进行连接客户端也会一直阻塞&…

CPP 核心编程9-STL

STL初识 2.1 STL的诞生 长久以来&#xff0c;软件界一直希望建立一种可重复利用的东西C的面向对象和泛型编程思想&#xff0c;目的就是复用性的提升大多情况下&#xff0c;数据结构和算法都未能有一套标准,导致被迫从事大量重复工作为了建立数据结构和算法的一套标准,诞生了S…

四、文件管理(二)目录

目录 2.0文件控制块和索引结点 2.1目录的结构 2.1.1单级目录结构 2.1.2两级目录结构 2.1.3树形目录结构 2.1.4有向无环图目录结构 2.2目录的操作 2.4文件共享 2.4.1基于索引结点&#xff08;硬链接&#xff09; 2.4.2基于符号链&#xff08;软链接&#xff09; 2.0文…

前端开发是做什么的?工作职责

想要了解前端从业者的工作职责&#xff0c;需要从一个完整网站应用产生流程入手&#xff0c;一个网站应用从无到有的过程大致如下 &#xff1a; 1&#xff09;产品经理与甲方反复沟通交流&#xff0c;逐步确定产品需求完成设计草图&#xff1b; 2&#xff09;产品经理根据需求…

如何在centos上安装nvidia docker

当基于nvidia gpu开发的docker镜像在实际部署时&#xff0c;需要先安装nvidia docker。那么如何安装nvidia docker呢。下面将详细介绍下。 安装原生docker yum -y install docker-io 下载nvidia docker安装包 我下载的是rpm文件&#xff0c;具体见截图 安装nvidia docker…

word如何给论文加引用文献

给论文加引用文献其实差不多就是加了个链接&#xff0c;通过点击链接跳转到文末最后展示引用文献额作者&#xff0c;论文名等等信息&#xff0c;给论文加引用文献只要有一下几步&#xff1a; 一、设置参考文献标号字体格式 对于论文中的文献&#xff0c;首先设置论文前序号的…

redis基础6——缓存穿透、缓存击穿、缓存雪崩

文章目录一、缓存穿透&#xff08;双库为空&#xff09;1.1 基础概念1.2 解决办法1.2.1 业务层校验1.2.2 设置key过期时间1.2.3 布隆过滤器1.2.3.1 原理1.2.3.1.1 哈希函数使用1.2.3.1.2 布隆过滤器数据结构1.2.3.1.2.1 映射函数执行过程1.2.3.1.2.2 布隆过滤器的误判率1.2.3.2…

华为机试 - 最大化控制资源成本

目录 题目描述 输入描述 输出描述 用例 题目解析 算法源码 题目描述 公司创新实验室正在研究如何最小化资源成本&#xff0c;最大化资源利用率&#xff0c;请你设计算法帮他们解决一个任务混部问题&#xff1a; 有taskNum项任务&#xff0c;每个任务有开始时间&#xf…

数字图像处理实验(一)|图像的基本操作和基本统计指标计算

文章目录一、实验目的二、实验主要仪器设备三、实验原理(1)将一幅图像视为一个二维矩阵。(2)利用MATLAB图像处理工具箱读、写和显示图像文件。(3)计算图像的有关统计参数。(4)改变图像尺寸、旋转图像、裁剪图像四、实验内容(1)用imwrite写入图像(2) 用imread读入一幅图像(自选)…