学懂现代C++——《Effective Modern C++》之转向现代C++

前言

现代C++中像auto、智能指针、移动语义等都是一些重大的优化特性,但也有一些像constexpr、nullptr等等这样一个小的特性。这章的内容就是这些小特性的集合。

条款7:在创建对象时注意区分()和{}

在现代C++中有3种方式来以指定的值初始化对象,分别时小括号、等号和大括号:

int x(0);  //初始化值在小括号中
int y = 0; //初始化值在等号后
int z{0};  //初始化值在大括号中

其中,大括号形式的初始化时C++11引入的统一初始化方式。大括号初始化可以应用的语境最为宽泛,可以阻止隐式窄化的类型转换,还对最令人苦恼的解析语法免疫。

先说阻止隐式窄化的类型转换,比如下面代码可以通过编译:

double x,y,z;int sum1(x+y+z);  //可以通过编译,表达式的值被截断为int
int sum2 = x+y+z; //同上

而以下代码不可以通过编译,因为大括号初始化禁止内建类型直接进行隐式窄化类型的转换。

int sum3{x+y+z};  //编译不通过

再说最令人苦恼的解析语法免疫。C++规定:任何能够解析为声明的都要解析为声明,而这会带来副作用。所谓最令人库娜的解析语法就是——程序员本来想要以默认方式构造一个对象,结果却不小心声明了一个函数。举个例子,我想调用一个没有形参的Widget构造函数,如果写成Widget w();,那结果就变成了声明了一个函数(名为w,返回一个Widget类型对象)而非对象。而用大括号初始化Widget w{};就不存在这个问题了。

但是,不能盲目的都使用大括号初始化。在构造函数被调用时,只要形参中没有任何一个具备std::initializer_list类型,那么大括号和小括号没有区别 ;如果又一个或多个构造函数声明了任何一个具备std::initializer_list类型的形参,那么采用了大括号初始化语法的调用语句会强烈地优先选用带有std::initializer_list类型形参的重载版本。也就是说,因为std::initializer_list的存在,大括号初始化和小括号初始化会产生大相径庭的结果。

这点最突出的例子是:使用两个实参来创建一个std::vector<数值类型>对象。std::vector有一个两个参数的构造函数,允许指定容器的初始大小(第一个参数),以及所有元素的初始值(第二个参数);但它还有一个std::initializer_list类型形参的构造函数。如果要创建一个元素为数值类型的std::vector(比如std::vector),并且传递两个实参给构造函数,那么使用大括号和小括号初始化的差别就比较大了:

std::vector<int> v1(10, 20); //创建一个含有10个元素的vector,所有元素的初始值都是20std::vector<int> v1{10, 20}; //创建一个含有2个元素的vector,元素的值分别时1,20

所以,如果是作为一个类的作者,最好把构造函数设计成客户无论使用小括号还是大括号都不会影响调用得重载版本才好。

条款8:优先选用nullptr,而非0或NULL

因为0和NULL都不是指针类型,而nullptr才是真正的指针类型。比如在重载指针类型和整型的函数时,如果使用0或者NULL调用这样的重载函数,则永远不会调用到指针类型的重载版本,只有使用nullptr才能调用到。当然为了兼容我们仍然需要遵循C++98的指导原则:避免在整型和指针类型之间重载。

条款9:优先选用别名声明,而非typedef

C++11提供了别名声明来替换typedef,两者作用在大部分情况下是一样的。比如下面的typedef:

typedef std::unique_ptr<<std::unordered_map<std::string, std::string>>> UPtrMapSS;typedef void (*FP)(int, const std::string&);

可以用下面的别名声明来替换:

using UPtrMapSS = std::unique_ptr<<std::unordered_map<std::string, std::string>>>;using FP = void (*)(int, const std::string&);

但还有有一种场景是只能使用别名声明的,那就是在定义模板的时候,typedef不支持模板化,但别名声明支持。在C++98中需要用嵌套在模板化的struct里的typedef才能达到相同效果。比如下面这段:

template<typename T>
struct MyAllocList {typedef std::list<T, MyAlloc<T>> type; //MyAllocList<T>::type是std::list<T, MyAlloc<T>>的同义词
};MyAllocList<Widget>::type lw; //客户代码

在C++11中用别名声明就很简单了:

template<typename T>
using MyAllocList = std::list<T, MyAlloc<T>>; //MyAllocList<T>是std::list<T, MyAlloc<T>>的同义词MyAllocList<Widget> lw; //客户代码

这里还可以看到,别名模板可以让人免写“::type”后缀。并且在模板内,对于内嵌typedef的引用经常要求加上typename的前缀,而别名模板没有这个要求。

条款10:优先选用限定作用域的枚举类型,而非不限作用域的枚举类型

推荐优先选用C++11提供的限定作用域的枚举类型有3个理由。第一,它可以降低名字空间的污染,因为限定作用域的枚举类型仅在枚举类型内可见。比如下面C++98的代码会报错:

enum Color { black, white, red}; // black、white、red和Color所在作用域相同auto white = false; // 编译报错!white在前面已经声明

而类似代码选用限定作用域的枚举类型则不会有问题:

enum class Color { black, white, red}; // black、white、red所在作用域限定在Color内auto white = false; // 没有问题

第二,它的枚举量是更强类型的,只能通过强制类型转换以转换为其他类型。这样可以避免奇怪的使用枚举值与数值类型比较的代码,真要使用时也必须进行一次强制转换来提醒这里有一个别扭的比较。

第三,限定作用域的枚举类型总是可以进行前置声明,而不限作用域的枚举类型却只有在指定了默认底层类型的前提下才可以进行前置声明。

还有一点需要记住,这两种枚举类型都支持指定底层类型。限定作用域的枚举类型默认底层类型是int。而不限作用域的枚举类型则没有默认底层类型,编译器会为枚举类型选择足够表示枚举值的最小类型,这也是为什么它不能直接进行前置声明,在没定义前编译器无法确认底层类型的。

条款11:优先选用删除函数,而非private未定义函数

C++11提供了使用“=delete”的方法将想阻止客户调用得函数标识为删除函数的方法,用以替代C++98中传统的将这些函数声明为private的方法。

删除函数的一个重要优点在于,任何函数都能成为删除函数,包括非成员函数和模板的具现。比如,我想定义一个判断是否是幸运数字的函数,因为隐式转换的存在会有一些奇怪的调用,而将他们定义为删除函数后就可以阻止这些奇怪的调用:

bool isLucky(int number);
bool isLucky(char) = delete;
bool isLucky(bool) = delete;
bool isLucky(double) = delete;// 下面奇怪的调用无法通过编译
if (isLucky('a')) ...
if (isLucky(true)) ...
if (isLucky(3.5)) ...

事实上,C++98中把函数声明为private并且不去定义,这样的实践想要的就是C++11中的删除函数实际达到的效果。前者作为后者的一种模拟动作,当然不如本尊来的好用。比如,前者无法应用于类外部的函数,也不总是能够应用于类内部的函数(类内部的函数模板)。就是它能用,也可能直到链接阶段才发挥作用。所以,请始终使用删除函数。

条款12:为意在改写的函数添加override声明

由于对于声明派生类中的改写,保证正确性很重要,而出错又很容易,C++11提供了一种方法来显示地标识派生类中的函数时为了改写基类版本:为其加上override。这样如果派生类中的改写出错,编译器在编译阶段就会报错。

它还有一个好处就是可以在你打算更改基类中虚函数的签名时,衡量以下波及的影响面。

条款13:优先选用const_iterator,而非iterator

const_iterator是STL中相当于指涉到const的指针的等价物。它们指涉到不可被修改的值。

C++11中获取和使用const_iterator相比于C++98变得很容易了。容器的成员函数cbegin和cend都返回const_iterator类型,甚至对于非const容器也是如此,并且STL成员函数若要取用指示位置的迭代器(例如,作插入或删除只用),它们也要求使用const_iterator类型。下面是一段C++11中使用const_iterator的示例代码:

std::vector<int> values;auto it = std::find(values.cbegin(), values.cend(), 1983);
values.insert(it, 1988);
条款14:只要函数不会抛出异常,就为其加上noexcept声明

当你明知道一个函数不会抛出异常却未给它加上noexcept声明的话,这就是接口规格缺陷。对于不会抛出异常的函数应用noexcept声明还有一个动机,那就是它可以让编译器生成更好的目标代码。相对于不带noexcept声明的函数,它有更多机会的得到优化。

noexcept性质对于移动操作,swap、内存释放函数和析构函数最有价值。默认地,内存释放函数和所有的析构函数都隐式地具备noexcept性质。

大多数函数都是异常中立的。此类函数自身并不抛出异常,但它们调用得函数则可能会抛出异常。当这种情况真的发生时,异常中立函数会允许该异常经由它传至调用栈的更深一层。异常中立函数用于不具备noexcept性质,因为它们可能会抛出这种“路过”的异常。

条款15:只要有可能使用constexpr,就使用它

constexpr对象都具备const属性,并由编译期已知的值完成初始化。所有的constexpr对象都是const对象,而并非所有的const对象都是constexpr对象。

constexpr函数在调用时若传入的实参值时编译器已知的,则会产生编译器结果。如果传入的值有一个或多个在编译期未知,则它的运作方式和普通函数无异,亦即它也是在运行期执行结果的计算。

在C++11中,constexpr函数不得包含多余一个可执行语句,即一条return语句。但在C++14中,这种限制被大大地放宽了,可以有多条语句。

条款16:保证const成员函数的线程安全性

保证const成员函数的线程安全性,除非可以确信它们不会用在并发语境中。

运用std::atomic类型的变量会比运用互斥量有更好的性能,因为其开销往往较小。

对于单个要求同步的变量或内存区域,使用std::atomic就足够了。但是如果有两个或更多个变量或内存区域需要作为以整个单位进行操作时,就要动用互斥量了。

条款17:理解特种成员函数的生成机制

特种成员函数是指那些C++会自行生成的成员函数:默认构造函数、析构函数、复制操作和移动操作。其中移动操作时C++11新增的,包括两个成员——移动构造函数和移动赋值运算符。示例如下:

class Widget {
public:
...
Widget(Widget&& rhs);               // 移动构造函数
Widget& operator=(Widget&& rhs);    // 移动赋值运算符
}

C++11中,特种成员函数的生成机制如下:

  • 默认构造函数:与C++98的机制相同。仅当类中不包含用户声明的构造函数时才生成。
  • 析构函数:与C++98的机制基本相同,唯一的区别在于析构函数默认为noexcept。与C++98的机制相同的是,仅当基类的析构函数为虚的,派生类的析构函数才是虚的。
  • 复制构造函数和复制赋值运算符:运行期行为与C++98相同——按成员进行非静态数据成员的复制构造和复制赋值。复制构造函数仅当类中不包含用户声明的复制构造函数时才生成,如果该类声明了移动操作则复制构造函数将被删除。复制赋值运算符仅当类中不包含用户声明的复制赋值运算符时才生成,如果该类声明了移动操作则复制赋值运算符将被删除。在已经存在显示声明的析构函数的条件下,生成复制操作已经成为了被废弃的行为。
  • 移动构造函数和移动赋值运算符:都按成员进行非静态数据成员的移动操作。仅当类中不包含用户声明的复制操作、移动操作和析构函数时才生成。

综上,如果想声明一个基类,且提供默认的移动操作和复制操作,则需要如下定义:

class Base {
public:virtual ~Base() = default;Base(Base&&) = default; //提供移动操作Base& operator=(Base &&) = default;Base(const Base&) = default; //提供复制操作Base& operator=(const Base &) = default;
}

这里解释一下:通常情况下,虚析构函数的默认实现就是正确的,而“=default”则是表达这一点的很好方式。不过,一旦用户声明了析构函数,移动操作的生成就被抑制了,而如果可移动性是能够支持的,加上“=default”就能够再次给予编译器以生成移动操作的机会。声明移动操作又会废除复制操作,所以如果还要可复制性,就再加一轮“=default”。

还有一点需要注意的是,成员函数模板在任何情况下都不会已知特种成员函数的生成。

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

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

相关文章

Web开发-新建Spring Boot项目

目录 Spring Boot 与 Web开发Spring Boot 与 MavenJava 环境搭建下载JDK下载xmapp下载navicat for mysql下载Eclipse配置tomcat配置maven 新建Spring Boot项目 Spring Boot 与 Web开发 Spring Boot 是一种用于简化 Spring 应用程序开发、部署和运行的框架&#xff0c;而 Web 开…

LeetCode01

LeetCode01 两数之和 给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和 为目标值 target 的那两个整数&#xff0c;并返回它们的数组下标。 你可以假设每种输入只会对应一个答案。但是&#xff0c;数组中同一个元素在答案里不能重复出现。 你…

【计算机网络】网络层和数据链路层

文章目录 IP协议网段划分分类划分法CIDR 方案路由NAT网络地址转换技术IP报文的另外三个参数mac帧ARP协议交换机ICMP代理服务器 IP协议 TCP有将数据 可靠、高效 发给对方的 策略&#xff0c;而IP具有发送的能力&#xff0c;即将数据从A主机送到B主机的 能力 。 用户要的是100%…

医院陪诊系统:改善患者体验的技术创新

医院陪诊系统是一种创新的技术解决方案&#xff0c;旨在提高患者在医疗机构的整体体验。它不仅为患者提供便利&#xff0c;还增加了医疗机构的效率。本文将深入探讨医院陪诊系统的技术创新&#xff0c;包括其关键功能和实现方法。 1. 医院陪诊系统的基本功能 医院陪诊系统的…

Yolov4 学习笔记

Yolov4 学习笔记 损失函数之前损失函数存在的问题GIOUDIOUCIOU BOSSPP netCSP net SAMPAN层mish激活函数 损失函数 666666 之前损失函数存在的问题 之前公式为1-IOU IOU (交集面积) / (并集面积) 如果没有相交&#xff0c;分母为0&#xff0c;那么就无法计算 分母接近零&#…

自学WEB后端05-Node.js后端服务链接数据库redis

嘿&#xff0c;亲爱的小伙伴们&#xff01;&#x1f604; 今天我要给大家分享一个超级方便且高效的 NoSQL 类型数据库——Redis&#xff01;&#x1f4a1; 它可不是一般的关系型数据库哦&#xff0c;而是以键值对形式存储数据的内存数据库。&#x1f4da; 快跟着我一起来学习如…

运动健身行业如何线上获客?

生活水平的提高带来的是居民消费力的上升&#xff0c;生活方式也快速转变&#xff0c;随之而来的健康问题也越来越多&#xff0c;人们开始更加关注自己的身体健康&#xff0c;运动健身行业也越来越热门&#xff0c;平常我们较常见到的营销都是线下营销&#xff0c;那么运动健身…

项目04-基于Docker的Prometheus+Grafana+AlertManager的飞书监控报警平台

文章目录 一.项目介绍1.流程图2.拓扑图3.详细介绍 二.前期准备1.项目环境2.IP划分 三. 项目步骤1.ansible部署软件环境1.1 安装ansible环境1.2 建立免密通道1.3 批量部署docker 2 部署nginx、MySQL以及cadvisor、exporter节点2.1 在nginx节点服务器上面配置nginx、node_exporte…

Linux系统-Ubuntu的下载和安装

第一章 Linux系统-Ubuntu的下载和安装 1.下载Ubuntu ​ 进入https://cn.ubuntu.com/download中文官网下载iso映像文件&#xff1a; 2.安装Ubuntu 1.打开虚拟机&#xff0c;点击创建新的虚拟机&#xff1a; 2.选择“典型”&#xff0c;然后点击“下一步”&#xff1a; 3.选择…

YUM 升级 PHP7

文章目录 YUM 升级 PHP71. 查看当前 PHP 信息2. YUM 安装 PHP73. 查看 PHP 版本4. 启动PHP-FPM YUM 升级 PHP7 参考地址&#xff1a;网站地址 参考地址&#xff1a;网站地址 1. 查看当前 PHP 信息 # 查看 PHP 版本信息 php -v# 查看 yum 源中 PHP 信息 yum list | grep php2. …

前端开发 vs. 后端开发:编程之路的选择

文章目录 前端开发&#xff1a;用户界面的创造者1. HTML/CSS/JavaScript&#xff1a;2. 用户体验设计&#xff1a;3. 响应式设计&#xff1a;4. 前端框架&#xff1a; 后端开发&#xff1a;数据和逻辑的构建者1. 服务器端编程&#xff1a;2. 数据库&#xff1a;3. 安全性&#…

27、Flink 的SQL之SELECT (窗口函数)介绍及详细示例(3)

Flink 系列文章 1、Flink 部署、概念介绍、source、transformation、sink使用示例、四大基石介绍和示例等系列综合文章链接 13、Flink 的table api与sql的基本概念、通用api介绍及入门示例 14、Flink 的table api与sql之数据类型: 内置数据类型以及它们的属性 15、Flink 的ta…

STM32cubeIDE 更改Repository folder

使用STM32CubeIDE时&#xff0c;会调用STM32CubeMX&#xff0c;但是这两个软件下载的更新包都放在C:/user/目录下面&#xff0c;而且文件很大&#xff0c;用不了多久就会把C盘填满&#xff0c;所以刚开始安装的时候就要把更新目录更换掉。具体更换方法如下&#xff1a; Window…

浅谈软件测试面试一些常见的问题

一、简历及岗位匹配度 说到简历&#xff0c;其实这一点是很重要但又被很多职场小白忽视的问题。经常有人说我投了很多简历&#xff0c;但是没有公司给我打电话&#xff0c;怎么办&#xff1f; 首先&#xff0c;应该明白的一点&#xff1a;面对求职市场的激烈竞争性&#xff0…

docker系列(7) - Dockerfile

文章目录 7. Dockerfile7.1 Dockerfile介绍7.2 指令规则7.3 指令说明7.3.1 RUN命令的两种格式7.3.1 CMD命令覆盖问题7.3.2 ENTRYPOINT命令使用7.3.3 ENV的使用 7.4 构建tomcat Dockerfile案例7.4.1 准备原始文件7.4.2 编写Dockerfile7.4.3 构建镜像7.4.4 验证镜像 7.5 构建jdk基…

LeetCode 接雨水 木桶理论、dp预处理

原题链接&#xff1a; 力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 题面&#xff1a; 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图&#xff0c;计算按此排列的柱子&#xff0c;下雨之后能接多少雨水。 示例 1&#xff1a; 输入&#xff1a…

计算机里的神灵(SCIP)

计算机程序的构造和解释 我找到计算机里的神灵了&#xff0c;开心一刻 下面是从MIT官网下载的 SCIP求值器&#xff08;解释器&#xff09;的代码&#xff0c;这个官网是个宝藏库 还有其他视频课程和 SCIP的问题答案和可运行代码 链接&#xff1a;https://ocw.mit.edu/courses/6…

VS2022 编译protobuf , qt 使用

一、下载源码 protobuf: 同步 https://github.com/protocolbuffers/protobuf (gitee.com) 下载如v3.11.2 版本 二、下载CMake 三、编译 1、在1处选择源码目录下的cmake 目录&#xff1b;在2处选择一处空目录&#xff08;自己随便建&#xff09; 2、点击config&#xff0c;选择…

系统架构设计师-大数据

目录 一、大数据 1、大数据架构 2、大数据技术生态 3、Lambda架构 4、Kappa架构 5、Lambda架构与Kappa架构对比 一、大数据 1、大数据架构 大数据是指其大小或复杂性无法通过现有常用的软件工具&#xff0c;以合理的成本并在可接受的时限内对其进行捕获、管理和处理的数据集。…

【rhce考试时间是每年什么时候呢?】

RHCE9.0 新技术 公开课 10月11日&#xff0c;12日 感兴趣可留言 如果你是一个系统管理员&#xff0c;或者正朝着这个方向努力前进&#xff0c;那么你可能已经听过RHCE这个词。RHCE是Red Hat Certified Engineer的缩写&#xff0c;是全球公认的Linux系统工程师认证之一。通过获…