status
Published
slug
modern-cpp-2
type
Post
category
Technology
date
May 18, 2023
tags
C++
笔记
summary
第16-30章节的重点知识记录
第16章 override和final说明符(C++11)第17章 基于范围的for循环(C++11 C++17 C++20)第18章 支持初始化语句的if和switch(C++17)第19章 static_assert声明第20章 结构化绑定(C++17 C++20)第21章 noexcept关键字(C++11 C++17 C++20)第22章 类型别名和别名模板第23章 指针字面量nullptr(C++11)第24章 三向比较(C++20)第25章 线程局部存储(C++11)第26章 扩展的inline说明符(C++17)第27章 常量表达式(C++11~C++20)第28章 确定的表达式求值顺序(C++17)第29章 字面量优化(C++11~C++17)第30章 alignas和alignof(C++11 C++17)
第16章 override和final说明符(C++11)
- 重写(override)、重载(overload)和隐藏(overwrite):
- 重写(override):派生类覆盖基类的虚函数,要求满足相同的函数名、形参列表以及返回类型
- 重载(overload):同一个类中提供了多个函数名相同的函数(形参列表不同),常见的使用为重载多个类构造函数
- 隐藏(overwrite):隐藏是指基类成员函数,无论它是否为虚函数,当派生类出现同名函数时,如果派生类函数签名不同于基类函数,则基类函数会被隐藏。如果派生类函数签名与基类函数相同,则需要确定基类函数是否为虚函数,如果是虚函数,则这里的概念就是重写;否则基类函数也会被隐藏。另外,如果还想使用基类函数,可以使用using关键字将其引入派生类。
- override说明符:C++11标准中提供
- 将override说明符放在虚函数尾部,可以明确告诉编译器这个虚函数需要覆盖基类虚函数,编译器一旦发现虚函数不符合重写规则,就会给出错误提示
没有override说明符时,虚函数不符合重写规则时,编译器并不会报错,运行时才可能出现问题
- final说明符:C++11标准中引入,告诉编译器该虚函数不能被派生类重写
- 声明在虚函数的尾部,该函数不能被重写
- 声明在类尾部:该类不能作为基类被继承
- override和final说明符的特殊之处:
- 为了保持兼容性,C++11标准中,override和final没有被作为保留的关键字
第17章 基于范围的for循环(C++11 C++17 C++20)
- 基于范围的for循环:C++11标准引入
- 隐藏了迭代器的初始化和更新过程,语法更简洁
- 语法格式:
- 范围表达式可以是数组或对象
- 对象必须满足以下2个条件中的任意一个
- 对象类型定义了begin和end成员函数
- 定义了以对象类型为参数的begin和end普通函数
- 示例
- 范围声明的两种形式:直接使用值、使用引用
- 一般来说,我们希望对于复杂的对象使用引用,而对于基础类型使用值,因为这样能够减少内存的复制。
- 如果不会在循环过程中修改引用对象,那么推荐在范围声明中加上const限定符以帮助编译器生成更加高效的代码
- 加上const可以避免不必要的复制
- begin和end函数不必返回相同类型
- 临时范围表达式陷阱
- 当range_expression是一个纯右值时,右值引用会扩展其生命周期,保证其整个for循环过程中访问的安全性。
- 当range_expression是一个泛左值时,for循环会引发一个未定义的行为,举例如下:foo()为泛左值
- 解决方法:将数据复制出来
范围循环底层实现基于的伪代码:
- 实现一个支持基于范围的for循环的类
- 对于在遍历容器过程中需要修改容器的需求,还是需要使用迭代器来处理。
第18章 支持初始化语句的if和switch(C++17)
将初始化语句和条件语句写在一行有助于代码阅读和整理,与此同时也能减少无谓的大括号和缩进,增加代码的可读性和可维护性
- 支持初始化语句的if:初始化的变量生命周期会伴随整个if结构
- 语法:
- 可以添加else、else if部分,其中else if的条件语句之前也可以使用初始化语句
- else if的初始化语句中声明的变量生命周期只存在在else if之后的语句中
- 使用场景:
- 基于生命周期贯穿整个if结构这点,可以利用该特性对if结构加锁
- 支持初始化语句的switch:
- switch初始化语句声明的变量的生命周期会贯穿整个switch结构
- 示例:
第19章 static_assert声明
静态断言可以将错误排查的工作前置到编译时
- 断言(assert):运行时断言
- 断言的行为比较粗暴,会直接显示错误信息并终止程序
- 断言只应该出现在需要表达式返回true的位置
- 程序运行到断言代码时才会触发断言
- 静态断言(static_assert):C++11标准引入的特性,满足以下要求
- 所有处理必须在编译期间执行,不允许有空间或时间上的运行时成本。
- 它必须具有简单的语法。
- 断言失败可以显示丰富的错误诊断信息。
- 它可以在命名空间、类或代码块内使用。
- 失败的断言会在编译阶段报错。
- 静态断言的使用:
- static_assert需要传入两个实参:常量表达式和诊断消息字符串。
- C++17标准支持单参数的static_assert
第20章 结构化绑定(C++17 C++20)
- 元组:C++11标准中引入了元组概念
- 结构化绑定:C++17标准中引入
- 结构化绑定是指将一个或者多个名称绑定到初始化对象中的一个或者多个子对象(或者元素)上,相当于给初始化对象的子对象(或者元素)起了别名
- auto:类型占位符
- [x, y]:绑定标识符列表
- x、y不是绑定目标的引用!
- x、y是绑定目标的别名
- return_multiple_values():结构化绑定目标,不一定是函数的返回结果,也可以是结构体或任意合理的表达式
- 实现可以修改目标的结构化绑定
- 使用结构化绑定无法忽略对象的子对象或者元素
- 结构化绑定的三种类型
- 原生数组:要求绑定标识符列表中别名的数量和数组数量一致
- 结构体和类对象
- 类和结构体中的非静态数据成员个数必须和标识符列表中的别名的个数相同
- 数据成员必须公有
- 数据成员必须在同一个类或者基类中
- 绑定的类和结构体中不能存在匿名联合体
- 元组和类元组的对象(pair、array满足类元组条件)
- 需要满足std::tuple_size<T>::value是一个符合语法的表达式,并且该表达式获得的整数值与标识符列表中的别名个数相同。
- 类型T还需要保证std::tuple_element<i, T>::type也是一个符合语法的表达式,其中i是小于std::tuple_size<T>::value的整数,表达式代表了类型T中第i个元素的类型。
- 类型T必须存在合法的成员函数模板get<i>()或者函数模板get<i>(t),其中i是小于std::tuple_size<T>::value的整数,t是类型T的实例,get<i>()和get<i>(t)返回的是实例t中第i个元素的值。
- 实现一个类元组类型
- 绑定的访问权限问题
- C++20标准规定结构化绑定的限制不再强调必须为公开数据成员
第21章 noexcept关键字(C++11 C++17 C++20)
- throw:在C++11标准前,throw可以声明函数是否抛出异常,并描述函数抛出的异常类型
表示不会抛出任何类型的异常
- noexcept:
- 作为说明符:用来说明函数是否会抛出异常
- 在函数名后添加noexcept,指示编译器这几个函数不会抛出异常
- noexcept还能接受一个返回布尔的常量表达式,当表达式评估为true的时候,其行为和不带参数一样,表示函数不会抛出异常。反之,当表达式评估为false的时候,则表示该函数有可能会抛出异常。
- noexcept对表达式的评估是在编译阶段执行的,因此表达式必须是一个常量表达式。
- 示例:
- 作为运算符:接受表达式参数并返回true或false(在编译阶段进行),表达式的结果取决于编译器是否在表达式中找到潜在异常
- 用noexcept来解决移动构造问题:
- 使用noexcept运算符判断目标类型的移动构造函数是否有可能抛出异常
- 如果没有抛出异常的可能,那么函数可以选择进行移动操作;否则将使用传统的复制操作。
- noexcept和throw():
- noexcept拥有更高的性能(具体原因书中有些不展开了)
- C++17和C++20逐步且完全移除了throw声明函数异常的方法
- 默认使用noexcept的函数:
- C++11标准规定下面几种函数会默认带有noexcept声明。
- 默认构造函数、默认复制构造函数、默认赋值函数、默认移动构造函数和默认移动赋值函数。
- 类型的析构函数以及delete运算符默认带有noexcept声明
- 使用noexcept的时机
- 一定不会出现异常的函数。通常情况下,这种函数非常简短,例如求一个整数的绝对值、对基本类型的初始化等。
- 当我们的目标是提供不会失败或者不会抛出异常的函数时可以使用noexcept声明
- 将异常规范作为类型的一部分:C++17标准将异常规范引入了类型系统。
第22章 类型别名和别名模板
- 类型别名:为了代码更加简洁,使用typedef为较长的类型名定义别名
- C++11标准提供了新的定义类型别名的方法
- 别名模板:别名模板本质上也应该是一种模板,它的实例化过程是用自己的模板参数替换原始模板的模板参数,并实例化原始模板。
- typedef+类型嵌套的实现方法
第23章 指针字面量nullptr(C++11)
- 零值整数字面量:
- C++标准中的特殊规则:0既是一个整型常量,又是一个空指针常量
- 使用0代表不同类型的规则给C++带来了二义性
- NULL在C++11标准之前本质为0,在C中为
(void *)0
- nullptr关键字:C++11标准中引入
- nullptr表示空指针的字面量,是一个std::nullptr_t类型的纯右值
- nullptr可以隐式转换为各种指针类型,但无法隐式转换到非指针类型
- nullptr不允许运用在算术表达式或者与非指针类型的比较中
- 引入nullptr后,可以为函数模板或类设计空指针类型的特化版本
第24章 三向比较(C++20)
- 名为spaceship的运算符
<=>
:C++20引入 - 三向比较运算符:两个比较的操作数lhs和rhs通过
<=>
比较可能产生3种结果,该结果可以和0比较,小于0、等于0或者大于0分别对应lhs <rhs
、lhs == rhs
和lhs > rhs
<=>
返回值只能与0和自身类型比较
- 三向比较的返回类型:
- std::strong_ordering
- 三种结果:
- std::strong_ordering::less
- std::strong_ordering::equal
- std::strong_ordering::greater
- std::strong_ordering中strong表达了一种可替换性,若lhs == rhs,那么在任何情况下rhs和lhs都可以相互替换,也就是fx(lhs) == fx(rhs)
- std::weak_ordering
- 三种结果:
- std::weak_ordering::less
- std::weak_ordering::equivalent
- std::weak_ordering::greater
- std::weak_ordering中weak表达了一种不可替换性,也就是fx(lhs) != fx(rhs)
- 当std::weak_ordering和std::strong_ ordering同时出现在基类和数据成员的类型中时,该类型的三向比较结果是std::weak_ordering
- std:: partial_ordering
- 四种结果:
- std::partial_ordering::less
- std::partial_ordering::equivalent
- std::partial_ordering::greater
- std::partial_ordering::unordered
- std::partial_ordering约束力比std::weak_ordering更弱,它可以接受当lhs == rhs时rhs和lhs不能相互替换
- std::partial_ ordering::unordered,表示进行比较的两个操作数没有关系。
- 对基础类型的支持:
- 对两个算术类型的操作数进行一般算术转换,然后进行比较。其中整型的比较结果为std::strong_ordering,浮点型的比较结果为std::partial_ordering。
- 对于无作用域枚举类型和整型操作数,枚举类型会转换为整型再进行比较,无作用域枚举类型无法与浮点类型比较
- 对两个相同枚举类型的操作数比较结果,如果枚举类型不同,则无法编译。
- 对于其中一个操作数为bool类型的情况,另一个操作数必须也是bool类型,否则无法编译。比较结果为std::strong_ordering。
- 不支持作比较的两个操作数为数组的情况
- 对于其中一个操作数为指针类型的情况,需要另一个操作数是同样类型的指针,或者是可以转换为相同类型的指针,比如数组到指针的转换、派生类指针到基类指针的转换等,最终比较结果为std::strong_ordering
- 自动生成的比较运算符函数:
- 标准库中提供了一个名为std::rel_ops的命名空间,在用户自定义类型已经提供了==运算符函数和<运算符函数的情况下,帮助用户实现其他4种运算符函数,包括!=、>、<=和≥
- C++20标准规定,如果用户为自定义类型声明了三向比较运算符,那么编译器会为其自动生成<、>、<=和>=这4种运算符函数。
- 兼容性问题
第25章 线程局部存储(C++11)
- 线程局部存储:
- 指对象内存在线程开始后分配,线程结束时回收且每个线程有该对象自己的实例
- 线程局部存储的对象独立于各个线程
- 操作系统和编程器对线程局部存储提供了支持,但他们都有各自的方法声明线程局部存储变量,在使用范围和规则上也存在一些区别
⇒ C++11标准中添加了thread_local说明符来声明线程局部存储变量
- thread_local说明符可以用来声明线程生命周期的对象,它能与static或extern结合,分别指定内部或外部链接
- static不影响线程局部存储的属性(声明周期)
- thread_local声明的变量多个线程属性,行为上非常像静态变量
- errno:线性局部存储的典型例子
- 在C++11标准之前errno是静态变量,之后被修改为一个线程局部存储变量
- 在同一个线程中,一个线程局部存储对象只会初始化一次,同样也只会销毁一次(通常发生走到线程退出的时刻)
第26章 扩展的inline说明符(C++17)
- 定义非常量静态成员变量,声明和定义必须分开:
- 对于整型、浮点类型等部分字面量类型,常量静态成员是可以一边声明一边定义的
- inline说明符:C++17标准中增强了inline说明符的能力,允许内联定义静态变量
- 编译器会在类 X的定义首次出现时对内联静态成员变量进行定义和初始化
第27章 常量表达式(C++11~C++20)
通过constexpr说明符声明常量表达式函数以及常量表达式值,它们让程序在编译期做了更多的事情,从而提高程序的运行效率
- 常量的不确定性:无法有效地要求变量和函数在编译阶段就计算出结果
补充——宏的使用:C++程序员应该尽量少使用宏,因为预处理器对于宏只是简单的字符替换,完全没有类型检查,而且宏使用不当出现的错误难以排查。
- 常量表达式:指值不会改变并且在编译过程能得到计算结果的表达式
- 用常量表达式初始化const对象也是常量表达式
- constexpr:C++11标准中定义的新关键字,能够有效地定义常量表达式,并且达到类型安全、可移植、方便库和嵌入式系统开发的目的。
- constexpr值:即常量表达式值
- 一个用constexpr说明符声明的变量或者数据成员,它要求该值必须在编译期计算
- 常量表达式值必须被常量表达式初始化
- constexpr函数:即常量表达式函数
- 常量表达式函数的返回值可以在编译阶段就计算出来
- 约束规则:
- 函数必须返回一个值,所以它的返回值类型不能是void。
- 函数体必须只有一条语句:return expr,其中expr必须也是一个常量表达式。如果函数有形参,则将形参替换到expr中后,expr仍然必须是一个常量表达式。
- 函数使用之前必须有定义。
- 函数必须用constexpr声明。
- constexpr构造函数:
- 构造函数必须用constexpr声明。
- 构造函数初始化列表中必须是常量表达式。
- 构造函数的函数体必须为空(这一点基于构造函数没有返回值,所以不存在return expr)
- 使用constexpr声明自定义类型的变量,必须确保这个自定义类型的析构函数是平凡的,否则也是无法通过编译的
- 平凡析构函数:
- 平凡析构函数必须满足下面3个条件。
- 自定义类型中不能有用户自定义的析构函数。
- 析构函数不能是虚函数。
- 基类和成员的析构函数必须都是平凡的。
- C++14标准对常量表达式函数的改进:
- 函数体允许声明变量,除了没有初始化、static和thread_local变量。
- 函数允许出现if和switch语句,不能使用go语句。
- 函数允许所有的循环语句,包括for、while、do-while。
- 函数可以修改生命周期和常量表达式相同的对象。
- 函数的返回值可以声明为void。
- constexpr声明的成员函数不再具有const属性。
- constexpr lambdas表达式:
- 从C++17开始,lambda表达式在条件允许的情况下都会隐式声明为constexpr
- constexpr的内联属性:C++17标准中,constexpr声明静态成员变量时,也被赋予了该变量的内联属性
- if constexpr:是C++17标准提出的一个非常有用的特性,可以用于编写紧凑的模板代码,让代码能够根据编译时的条件进行实例化。
- if constexpr的条件必须是编译期能确定结果的常量表达式。
- 条件结果一旦确定,编译器将只编译符合条件的代码块
- if constexpr不支持短路规则
- C++20标准的修改:
- 允许constexpr虚函数
- 允许constexpr函数中的Try-catch
- 允许在constexpr中进行平凡的默认初始化:放松了对constexpr上下文对象默认初始化的要求
- 允许在constexpr中更改联合类型的有效成员
- 允许dynamic_cast和typeid出现在常量表达式中
- 允许在constexpr函数使用未经评估的内联汇编。
- 立即函数:该函数需要使用consteval说明符来声明
- 确保函数在编译期就执行计算,对于无法在编译期执行计算的情况则让编译器直接报错
- constinit说明符:主要用于具有静态存储持续时间的变量声明,要求变量具有常量初始化程序
- 为了解决由于静态初始化顺序错误导致的问题(Static Initialization Order Fiasco),C++20标准引入constinit,帮助检查变量是否符合常量初始化程序
- std::is_constant_evaluated:用于检查当前表达式是否是一个常量求值环境,如果在一个明显常量求值的表达式中,则返回true;否则返回false。
- 该函数通常会用于代码优化中,比如在确定为常量求值的环境时,使用constexpr能够接受的算法,让数值在编译阶段就得出结果。
- 明显常量求值:
- 常量表达式,这个类别包括很多种情况,比如数组长度、case表达式、非类型模板实参等。
- if constexpr语句中的条件。
- constexpr变量的初始化程序。
- 立即函数调用。
- 约束概念表达式。
- 可在常量表达式中使用或具有常量初始化的变量初始化程序。
第28章 确定的表达式求值顺序(C++17)
- 表达式求值顺序的不确定性
- 表达式求值顺序详解(C++17标准对于表达式求值顺序进行了改善)
- 函数表达式一定会在函数的参数之前求值
- 在foo(a, b, c)中,foo一定会在a、b和c之前求值
- 对于后缀表达式和移位操作符而言,表达式求值总是从左往右
- 对于赋值表达式,表达式求值总是从右往左
- 对于new表达式的内存分配总是优先于构造函数中参数的求值
- 涉及重载运算符的表达式的求值顺序应由与之相应的内置运算符的求值顺序确定,而不是函数调用的顺序规则
第29章 字面量优化(C++11~C++17)
- 十六进制浮点字面量:C++11标准引入了std::hexfloat和std::defaultfloat用于修改浮点输入和输出的默认格式化
- std::hexfloat可以将浮点数格式化为十六进制的字符串
- std::defaultfloat可以将浮点数格式还原到十进制
- 二进制整数字面量:C++14标准中定义
- 二进制整数字面量有前缀0b和0B
- 单引号作为整数分隔符:
- 原生字符串字面量:
- 语法:prefix和delimiter都是可选部分
- 添加delimiter可以改变编译器对原生字符串字面量范围的判定,从而顺利编译带有)"的字符串
- prefix声明4个类型字符串的前缀L/u/U/u8
- 原生字面量除了能连接原生字符串字面量以外,还能连接普通字符串字面量
- 原生字符串不需要\r\n
prefix R"delimiter(raw_characters)delimiter”
- 用户自定义字面量:C++11标准中新引入了一个用户自定义字面量的概念,程序员可以通过自定义后缀将整数、浮点数、字符和字符串转化为特定的对象
- 代码示例:
第30章 alignas和alignof(C++11 C++17)
- 不可忽视的数据对齐问题:
- 为什么需要数据对齐?
- 硬件需要,好的数据对齐字节长度可以让提高CPU运行效率
- 好的对齐字节长度和CPU访问数据总线的宽度有关
- 控制数据对齐方法:
- C++11标准之前:使用offsetof间接实现,不同编译器有不同的扩展功能类控制类型的对齐字节长度
- alignof运算符:获取类型的对齐字节长度
- C++标准规定alignof只能接受类型
- 使用alignof还可以获得类型std::max_align_t的对齐字节长度
- alignas说明符:该说明符可以接受类型或者常量表达式,用于对声明的内容设置对齐字节长度
- C++11标准还提供了std::alignment_of、std::aligned_storage和std::aligned_union类模板型以及std::align函数模板来支持对于对齐字节长度的控制
- 使用new分配指定对齐字节长度的对象:C++17标准
- 通过让new运算符接受一个std::align_ val_t类型的参数来获得分配对象需要的对齐字节长度来实现的