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

1.前言

C++规则的设计目标之一是保证“类型错误”绝对不可能发生。理论上如果你的程序很顺利的通过编译,就表示它并不企图在任何对象身上执行任何不安全,无意义的操作。这是个极具价值的保证,可别草率的放弃它。

不幸的是,转型(cast)破坏了类型系统(type system)。那可能导致任何种类的麻烦,有些容易识别,有些容易隐晦。如果你来自c,java,c#阵营,请特别注意,因为那些语言中的转型(cast)比较必要而无法避免,相对来说也不危险,但c++中转型是一个你会想带着极大的尊重去亲近的一个特性。

2.转型(cast)知识点的回顾

首先,让我们回顾转型语法,因为通常有三种不同的形式,可写出相同的转型动作。c风格的转型动作看起来像这样:

(T)expression//将expression转型为T
函数风格的转型动作看起来像这样:
T(expression)//将expression转型为T

两种形式并无差别,纯粹只是小括号的摆放位置不同而以。称此两种形式为“旧式转型”。

C++还提供四种新式转型(常被称为c++ style casts)

const_cast<T>(expression)
dynamec_cast<T>(expression)
reinterpret_cast<T>(expression)
static_cats<T>(expression)

上述转换类型各有不同的作用:

const_cast通常被用来将对象的常量性转除(cast away the constness),它也是唯一有此能力的c++-style转型操作符;

dynamic_cast主要用来执行“安全向下转型”,也就是用来决定某对象是否继承体系中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作;

reinterpret_cast意图执行低级转型,实际动作可能取决于编译器,这也表示它不可移植。例如将一个pointer to int 转型为int。这一类转型在低级代码以外很少见。本来只使用一次,那是在讨论如何针对原始内存写出一个调试用的分配器;

static_cast用来强迫隐式转换,例如将non-cast对象转为const对象,或将int转为double等等。它也可以用来执行上述多种类型的反转换,例如将void*指针转为typed指针,将pointer-to-base转为pointer-to-derived。但它无法将const转为non-const,这个只有const_cast能办到;

旧式转型仍然合法,但新式转型较受欢迎。原因是:

(1)很容易在代码中被识别出来;

(2)各转型动作的目标愈窄化,编译器越可诊断出错误的运用。比如你如果打算将常量性去掉,除非使用新式转型中的const_cast,否则无法通过编译。

目前唯一使用旧式转型的时机:当我要调用一个explict构造函数将一个对象传递给一个函数时。例如:

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

从某个角度来说:刻意的“对象生成”动作感觉不怎么像“转型”,所以我很可能使用函数风格的转型动作而不使用static_cast。但我要提醒下,当我们日后出错导致coredump的代码时,编写的时候我们往往觉得说的过去,所以最好是忽略自己的主管想法,始终理智地使用新式转型。

许多程序员认为,转型其实什么都没做。只是告诉编译器把某种类型看作另一种类析。这是种错误的观念,任何一个类型转换(不论是通过转型操作而进行的显式转换,或通过编译器完成的隐式转换)往往真的令编译器编译出运行期间执行的码。比如在下面这段程序中:

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

将int x转型为double几乎肯定会产生一些代码,因为在大部分计算器体系结构中,int的底层表述不同于double的底层表述。这或许不会让你惊讶,但下面这个例子就有可能让你稍微睁大眼睛:

class Base{....};
class Derived:public Base{...};
Derived d;
Base* pb=&d;//隐喻地将derived*转换成base*

这里我们不过是建立一个base class指针指向一个derived class对象,但有时候上述的两个指针值并不相同。这种情况下会有个偏移量(offset)在运行期间被施行于derived*指针身上,用以取得正确的Base*指针值。

上述例子表明,单一对象可能拥有一个以上的地址(例如“”以base指向它时的地址和以derived指向它时的地址)。实际上一旦使用多重继承,这事情几乎一直发生者。即使在单一继承中也可能发生。虽然这还有其它含义,但至少意味你通常应该避免做出“对象在c++中如何布局”的假设。当然更不应该以此为假设执行任何转型动作。例如将对象地址转型为char*指针然后在它们身上进行指针算术,这样总会导致无定义(不明确)行为。

但请注意,我说的是有时候需要一个偏移量。对象的布局方式和它们的地址计算方式随编译器的不同而不同,那意味着由于知道对象如何布局而设计的转型,在某一平台行的通,再另一平台并不一定行的通。

另一件关于转型的有趣的事情是:我们很容易写出某些似是而非的代码。比如许多应用框架都要求derived class内的virtual函数代码的第一个动作就是先调用base class的对应函数。假设我们有个Window base class和一个SpecialWindow derived class,两者都定义了virtual函数的onResize,进一步假设SpeciaWindow的onResize函数被要求首先调用Window的onResize,下面是实现方式之一,相关程序看起来是对的,实际上是有问题的:

class Window{public:virtual void onResize(){....//base onResize实现代码}};class SpecialWindow:public Window{//derived classpublic:virtual void onResize(){        //derived onResize实现代码static_cast<Window>(*this).onResize();//将*this转型为Window,然后调用其                                    //onResize;这不可行。...//这里进行    SpecialWindow专属行为}}

上面代码中强调了转型动作(那是个新式转型,但若使用旧式转型也不能改变以下事实)。如预期的那样,这段程序将*this转型为Window,对函数onResize的调用也因此调用了Window::onResize。但实际上,它调用的并不是当前对象上的函数,而是稍早转型动作所建立的一个“this对象之base class成分”的暂时副本身上的onResize。再强调一次,上述代码并非在当前对象身上调用Window::onResize之后又在该对象身上执行SpecialWindow专属动作,它是在“当前对象之base class成分”的副本上调用Window::onResize,然后在当前对象身上执行SpeciaWindow专属动作。如果Winodw::onResize修改了对象内容,当前对象其实没被改动,改动的是副本。然而SpecialWindow::onResize内如果也修改对象,当前对象真的会被改动。这使得当前对象进入一种"伤残"状态:其base class成分的更改没有落实,而derived class成分的更改倒是落实了。

解决的方法是换掉转型动作,而不是将*this视为一个base class对象,你只是想调用base class版本的onResize函数,令它作用于当前对象身上。所有这样编写代码:

class SpecialWindow:public Window{public:virtual void onResize(){Window::onREsize();//调用Window::onResize作用于*this身上...}...
};

这个例子也说明:如果自己打算转型,可能会面临者将局面发展至错误的方向上。如果用的是dynamic_cast更是如此

在研究dynamic_cast设计意义之前,值的关注的是dynamic_cast的许多实现版本执行速度相当慢,至少有一个很普遍的实现版本基于“class名称之字符串比较”,如果在四层深的单继承体系内的某个对象身上执行dynamic_cast,刚才说的那个实现版本所提供的每一次dynamic_cast可能会耗用多达四次的strcmp调用,用于比较class名称。深度继承或多重继承的成本更高。

之所以需要dynamic_cast,通常是因为你想在一个你认定为derived class对象身上执行derived class操作函数,但你的手上却只有一个“指向base”的pointer或reference,你只能靠它们来处理对象。有两个一般性做法可以避免这个问题。

第一:使用容器并在其中存储直接指向derived class对象的指针,如此便消除了“通过base class接口处理对象”的需要。假设先前的Window/SpecialWindow继承体系中只有SpecialWindow才支持闪烁效果,试着不要这样做:

class Window{...};
class SpecialWindow:public Window{public:void blink();...
};typedef
std::vector<std::trl::shared_ptr<Window>>  VPW;
VPW winPtrs;
...
for(VPW::iterator iter=winPTRS.begin();iter!=winPtrs.end();++iter){//不希望使用//dynamic_castif(SpecialWindow* psw=dynamic_cast<SpecialWindow*>(iter->get())){psw->blink();}
}

应该改为这样做:

typedef std::vector<std::trl::shared_ptr<SpecialWindow>> VPSW;
VPSW winPtrs;
...for(VPSW::iterator iter=winPTRS.begin();iter!=winPtrs.end();++iter){(*iter)->blink();//这样写比较好,不需要使用dynamic_cast
}

当然了,这种做法使得你无法在同一容器内存储指针“指向所有可能之各种Window派生类”。如果真的要处理多种窗口类型,你可能需要多个容器,它们都必须具备类型安全性。

另一种做法可让你通过base class接口处理“所有可能的各种Window派生类”,那就是在base class内提供virtual函数做你想对各种Window派生类做的事。举个例子,虽然只有SpecailWindows可以闪烁,但或许将闪烁函数声明于base class内并提供一份缺省实现码是有意义的:

class Window{public:virtual blink()=0;...
}
class SpecialWindow:public Window{public:override blink(){....}...
};
typedef std::vector<std::trl::shared_ptr<Window>> VPW;
VPW winPtrs;//容器,内含指针,指向可能的Window类型
...
for(VPW::iterator iter=winPtrs.begin();iter!=winPtrs.end();++iter){(*iter)->blink();//注意,这里没有dynamic_cast。
}

无论是哪一种写法-“使用类型安全容器”或“将virtual函数往继承体系上方移动”,都并非放之四海皆准,但在许多情况下它们都提供一个可行的dynamic_cast替代方案。当它们起作用时,应该欣然拥抱它们。

绝对避免的一件事情是所谓的“连串dynamic_casts”,也就是看起来像这样的东西:

class Window{...
};
...//derived classes定义在这里
typedef std::vector<std::trl::shared_ptr<Window>> VPW;
VPW winPtrs;
...
for(VPW::iterator iter=winPtrs.begin();iter!=winPtrs.end();++iter){if(SpecialWindow1* psw1=dynamic_cast<SpecialWindow1*>(iter->get())){...}else if(SpecialWindow2* psw2=dynamic_cast<SpecialWindow2*>(iter->get())){...}else if(SpecialWindow3* psw3=dynamic_cast<SpecialWindow3*>(iter->get())){...}...
}

这样产生的代码又大又慢,而且基础不稳,因为每次Window class继承体系一有改变,所有这一类代码必须再次检阅看看是否需要修改。一旦加入新的derived class,或许上述连串判断中需要加入新的条件分支,这样的代码应该总是以某些“基于virtual函数调用”的东西取而代之。

良好的c++代码很少使用转型,但若说要完全摆脱它们又不切实际,例如将int转型为double就是转型的一个通情达理的使用,虽然它并非绝对必要,就像面对众多蹊跷可疑的构造函数一样,我们应该尽可能隔离转型动作,通常是把它隐藏在某个函数内,函数的接口会保护调用者不受函数内部任何动作影响。

3.总结

由于本文内容较多,将以上内容总结为以下几点:

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

2.如果转型是必要的,试着将它隐藏在某个函数背后。客户随后可以调用该函数,而不需要将转型放进它们的代码内;

3.一旦不得不使用转型,宁可使用c++style新式转型,不要使用旧式转型。前者很容易辨识出来,而且也相对职责分明。

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

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

相关文章

AOP切入点表达式和使用连接点获取匹配到的方法信息

目录 第一种 execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?) 第二种 annotation(com.itheima.anno.Log 首先&#xff0c;自定义一个注解&#xff0c;可以自己随意命名&#xff1a; 第一种 execution(访问修饰符? 返回值 包名.类名.?方法名…

Explain工具-SQL性能优化

文章目录 SQL性能优化的目标Explain中type效率级别&#xff08;重要&#xff09;注意 Explain覆盖索引ExplainindexExplainfilesortExplainfilesort创建 idx_bd(b,d) SQL性能优化的目标 达到 range 级别 Explain中type效率级别&#xff08;重要&#xff09; 显示的是单位查询…

工作流JBPM笔记:了解JBPM

一、什么是工作流 工作流管理联盟&#xff08;WFMC&#xff09;把工作流定义为&#xff1a;全部或部分由计算机支持或自动处理的业务过程。 工作流管理系统&#xff08;Workflow Management System&#xff0c;WFMS&#xff09;用来支持流程定义、管理和执行一批设定好的工作…

pytorch强化学习(1)——DQNSARSA

实验环境 python3.10 torch2.1.1 gym0.26.2 gym[classic_control] matplotlib3.8.0 numpy1.26.2DQN代码 首先是module.py代码&#xff0c;在这里定义了网络模型和DQN模型 import torch import torch.nn as nn import numpy as npclass Net(nn.Module):# 构造只有一个隐含层的…

Qt容器QToolBox工具箱

# QToolBox QToolBox是Qt框架中的一个窗口容器类,常用的几个函数有: ​setCurrentIndex(int index):设置当前显示的页面索引。可以通过调用该函数,将指定索引的页面设置为当前显示的页面。 addItem(QWidget * widget, const QString & text):向QToolBox中添加一个页面…

Flink系列之:分组聚合

Flink系列之&#xff1a;分组聚合 一、DISTINCT 聚合二、GROUPING SETS三、ROLLUP四、CUBE五、HAVING 适用于流、批 像大多数数据系统一样&#xff0c;Apache Flink支持聚合函数&#xff1b;包括内置的和用户定义的。用户自定义函数在使用前必须在目录中注册。 聚合函数把多行…

flutter学习-day10-布局类组件

&#x1f4da; 目录 介绍布局原理和约束盒模型布局 约束容器ConstrainedBox非约束容器UnconstrainedBox 线性布局 行row列column 弹性布局流式布局 WrapFlow 层叠布局对齐和相对定位布局构建回调 LayoutBuilder布局过程中AfterLayout布局完成后执行 本文学习和引用自《Flutte…

LLM大语言模型(二):Streamlit 无需前端经验也能画web页面

目录 问题 Streamlit是什么&#xff1f; 怎样用Streamlit画一个LLM的web页面呢&#xff1f; 文本输出 页面布局 滑动条 按钮 对话框 输入框 总结 问题 假如你是一位后端开发&#xff0c;没有任何的web开发经验&#xff0c;那如何去实现一个LLM的对话交互页面呢&…

Python MySQL数据库连接与基本使用

一、应用场景 python项目连接MySQL数据库时&#xff0c;需要第三方库的支持。这篇文章使用的是PyMySQL库&#xff0c;适用于python3.x。 二、安装 pip install PyMySQL三、使用方法 导入模块 import pymysql连接数据库 db pymysql.connect(hostlocalhost,usercode_space…

Spring MVC开发流程

1.Spring MVC环境基本配置 Maven工程依赖spring-webmvc <dependency><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifactId><version>5.1.9.RELEASE</version> </dependency>web.xml配置Dispatche…

NSSCTF第16页(2)

[NSSRound#4 SWPU]1zweb(revenge) 查看index.php <?php class LoveNss{public $ljt;public $dky;public $cmd;public function __construct(){$this->ljt"ljt";$this->dky"dky";phpinfo();}public function __destruct(){if($this->ljt"…

【力扣100】73.矩阵置零

添加链接描述 class Solution:def setZeroes(self, matrix: List[List[int]]) -> None:"""Do not return anything, modify matrix in-place instead."""# 思路是1.记录每一个0元素的行和列下标 2.遍历全数组row_index[]column_index[]mlen(…

day01unittest复习,断言

1.unittest 方法执行前 # def setUp(self) -> None: # print(方法执行前执行) # # def tearDown(self) -> None: # print(方法执行后执行一次) 2.unittest 类方法执行前后执行一次 classmethod def setUpClass(cls) -> None:print(类执行前执行一次)classm…

41、BatchNorm - 什么是批归一化

在 CNN 网络中有一个很重要的技术,叫作批归一化(bn, BatchNorm )。 归一化层一般位于卷积的后面,学术或者工程上,一般习惯将卷积+批归一化+激活统一成一个小的网络结构,比如口语化上称为conv+bn+relu。 这是因为基本上卷积后面肯定会有批归一化,而后面肯定会接激活函数…

微分和导数(一)

1.微分&#xff1a; 假设我们有⼀个函数f : R → R&#xff0c;其输⼊和输出都是标量。如果f的导数存在&#xff0c;这个极限被定义为 如果f′(a)存在&#xff0c;则称f在a处是可微的。如果f在⼀个区间内的每个数上都是可微的&#xff0c;则此函数在此区间中是可微的。导数f′…

网络协议 - UDP 协议详解

网络协议 - UDP 协议详解 UDP概述UDP特点UDP的首部格式UDP校验 參考文章 基于TCP和UDP的协议非常广泛&#xff0c;所以也有必要对UDP协议进行详解。 UDP概述 UDP(User Datagram Protocol)即用户数据报协议&#xff0c;在网络中它与TCP协议一样用于处理数据包&#xff0c;是一种…

必要时进行保护性拷贝

保护性拷贝&#xff08;Defensive Copy&#xff09;是一种常见的编程实践&#xff0c;用于在传递参数或返回值时&#xff0c;创建副本以防止原始对象被意外修改。以下是一个例子&#xff0c;展示了何时进行保护性拷贝&#xff1a; mport java.util.ArrayList; import java.uti…

成功解决 Plugin ‘org.springframework.boot:spring-boot-maven-plugin:‘ not found

Plugin ‘org.springframework.boot:spring-boot-maven-plugin:‘ not found的解决方案&#xff0c;亲测可用&#xff01; 方法一&#xff1a;清理IDEA的缓存 File -> Invalidate Caches 方法二&#xff1a;添加版本号 先看自己当前的版本号 首先打开pom.xml文件进行查看C…

数据手册Datasheet解读-肖特基二极管笔记

数据手册Datasheet解读笔记1-肖特基二极管 数据手册大体结构共包含10个部分肖特基二极管-SS14第一重点关注点&#xff1a;极限值第二重点关注点&#xff1a;电气特性 数据手册大体结构共包含10个部分 1.Features一特性 2.Application一应用 3.Description一说明4.Pin Configur…

关于在Java中打印“数字”三角形图形的汇总

之前写过一篇利用*打印三角形汇总&#xff0c;网友需要查看可以去本专栏查找之前的文章&#xff0c;这里利用二维数组嵌套循环打印“数字”三角形&#xff0c;汇总如下&#xff0c;话不多说&#xff0c;直接上代码&#xff1a; /*** 打印如下数字三角形图形*/ public class Wo…