LearnCPP
参考资料与入门
基础规则与运行机制
基础行为与生命周期
- UB 未定义行为
- 使用未初始化的内部类型变量
- 不返回值的有返回值函数 (
main 会隐式返回 0)
- 类型, 模板, 内联函数和内联变量在不同文件中重复定义且不相同
- 表达式求出的值超出其类型的表示范围
- 悬空指针解引用
- 悬空视图访问
- 未指定行为
- 特殊标识符
override final import module 不是关键字, 但是特定情况具有特殊含义
- 全局变量初始化
- 在
main 之前初始化, 所以它们的构造函数先于 main 执行
- 首先进行静态初始化, 然后进行动态初始化
- 静态初始化: 零初始化 + 常量初始化
- 动态初始化: 非常量初始化 (运行时求值)
- 初始化顺序未指定, 不要依赖于某个全局变量先于另一个初始化
- 生命周期与作用域
- 生命周期是对象的运行时概念, 作用域是标识符的编译时概念
- 在作用域外无法访问对象, 但对象可能依然存在
- 离开作用域等同于生命周期结束, 对象在此之后销毁
- 进入作用域等同于生命周期开始, 对象在此之前创建
- 临时对象在完整表达式结束时销毁
命名空间与链接
::foo 指定全局命名空间的 foo
- 前向声明也需要命名空间限定
namespace A = B::C; 创建命名空间别名
- 在命名空间内部声明的变量也是全局变量, 只是需要命名空间限定符访问
using 语句的作用域仅为当前作用域
- 匿名命名空间用于给所有内部函数加上内部链接
- 内联命名空间用于版本控制
- 内联命名空间内的标识符可直接通过外部命名空间访问 (但不影响链接属性)
FOO::foo 默认认为是变量
- 如果是类型, 需要
typename FOO::foo 明确指出
存储类说明符: static extern thread_local mutable
- 内部链接
static 变量为静态存储期, 全局变量为内部链接, 局部变量为无链接
- 常量变量默认为内部链接, 需要
extern 才能有外部链接
static 函数为内部链接, 否则默认为外部链接
- 我们当然不满足于变量与函数, 匿名命名空间可使其内部的所有标识符均为内部链接
- 外部链接
extern 全局变量为静态存储期, 外部链接
- 其前向声明为无初始化器的
extern 声明
- 如果想定义一个未初始化的非
const 全局变量, 不要使用 extern, 会被误解为前向声明
声明、定义与 ODR
- 声明与定义
- 所有定义都是声明, 非定义的声明称为纯声明
- 纯声明包括变量, 函数, 类的前向声明
- 标识符可用性
- 模板与类的标识符必须有完整定义才能使用
- 变量与函数的标识符只需有声明即可使用
- 一次定义规则 (ODR - One Definition Rule)
- 在一个文件内, 给定作用域内的每个函数, 变量, 类型或模板只能有一个定义 (编译器重定义错误)
- 在一个程序内, 给定作用域内的每个函数或变量只能有一个定义 (链接器重定义错误)
- 类型, 模板, 内联函数和内联变量允许在不同文件中具有重复定义, 只要每个定义都相同 (未定义行为)
宏与预处理
#define FOO 会将所有 FOO 替换为空, 包括注释和字符串字面值内的 FOO
- 但是
#if defined(FOO) / #ifdef 会检测宏是否定义, 不会展开宏
- 还有
# if !defined(FOO) / #ifndef
- 但但是
#if 与 #elif 中的表达式会展开宏
module
- 类似默认最佳实践的头文件 -> BMI (二进制模块接口)
- 仅一次编译
- 非
export 的符号不泄漏, 不受宏污染
- 导入顺序不重要
- CMake 3.28+ 完整支持模块, 其推荐的模块文件扩展名为
.cppm (Clang)
- 语法
export module Name; 定义模块接口单元
export 可导出函数 / 类型 / 变量, 否则为模块私有
import Name; 导入模块, 导入的不是源代码, 而是编译器生成的 BMI (二进制模块接口), 其中包含编译优化所需的所有信息
module private; 之后的代码定义模块实现单元, 实现隐藏细节的单文件模块
模块分区
// 文件 1
export module A:B;
import A:C; // 内部分区之间可以互相 import
export void func() {
// ...
}
// 文件 2
export module A:C;
import A:B; // 内部分区之间可以互相 import
export void func2() {
func();
}
// 文件 3
export module A; // 主模块接口
export import :B; // 导出导入, 将分区的内容重新暴露给外部用户
export import :C;
// 用户端
import A; // 只需要导入主模块接口, 就能访问所有分区的内容
- 子模块 (A.B) 不是特殊机制, A 与 A.B 之间没有任何关系
module; // 开启全局模块片段
#include <iostream> // 这里的宏和符号属于全局作用域
#include <vector>
export module MyData; // 真正的模块定义从这里开始
export void print_vec(const std::vector<int>& v) {
for (auto i : v) std::cout << i << " ";
}
型别推导
模板参数推导
类型系统
基本类型
- 不完整类型
- void 类型
- 表示无类型, 是故意的不完整类型, 无法实例化
- 函数返回类型为 void 表示无返回值
- 函数参数类型为 void 表示无参数 (已弃用)
- void 指针可以指向任何类型的数据, 但不能解引用
- 类型转换
- 混用无符号与有符号整数类型会触发整数提升, 导致结果一定为无符号类型
- 固定宽度整数 (
cstdint)
int8_t int16_t int32_t int64_t
uint8_t uint16_t uint32_t uint64_t
- 注意
int8_t 与 uint8_t 可能被定义为 char 类型
- 大小与环绕
std::size_t 是某个无符号整数类型, 最好通过 <cstddef> 引入
sizeof 运算符返回该类型, 但你不需要显式引入
- 构造一个其对象表示中的字节数超过
std::size_t 类型中可表示的最大值的类型是格式错误的
- 一些编译器将可创建的最大对象限制为
std::size_t 类型的最大值的一半 (保证指针减法结果在 std::ptrdiff_t 范围内)
- 无符号整数转换为有符号整数时, 发生模数环绕 (C++ 20)
初始化
int a; // 默认初始化
int a = 0; // 复制初始化
int a(0); // 直接初始化
int a{0}; // 列表初始化, 禁止窄化转换
int a = {0}; // 会被优化为列表初始化, 但实际是拷贝列表初始化, 受限于拷贝初始化规则
int a{}; // 值初始化
[[maybe_unused]] int a{}; // 忽略未使用变量的编译器警告
- 默认初始化
- 复制初始化
- 基本类型直接赋值
- 曾经 RVO 不强的情况下,
My_class a = My_class() 可能产生额外的构造开销
- 也用于隐式复制值, 如按值将参数传递给函数, 按值从函数返回或按值捕获异常是构造的临时对象
- 直接初始化
- 调用构造函数
My_class a = static_cast<My_class>(b) 强制转换就是直接初始化 myclass a = My_class tmp(b)
- 列表初始化
- 值初始化
- 基本类型触发零初始化
- 类类型调用默认构造函数, 没有默认构造函数则递归触发零初始化, 再调用编译器合成的默认构造函数
类型限定符: const 与 volatile
const
- 对于基本类型, 返回类型上的
const 限定符会被忽略
- 对于类类型, 返回类型上的
const 限定符会阻碍某些类型的编译器优化
- 常量分类
- 编译时常量: 字面量 + 初始化器为编译时常量的常量对象
- 运行时常量: 常量函数参数 + 初始化器不是编译时常量的常量对象
- 常量表达式
- 只能调用
constexpr 函数
- 定义: 字面量, 常量变量, 运算符和函数调用的非空序列
- 要求所有组成部分都是编译时可计算的
- 包括: 带有常量表达式参数的
constexpr 函数调用, 枚举器, 类型特性, constexpr lambda 表达式, 非类型模板参数
- 注意: 没有带有常量表达式初始化器的
const 非整型类型 (同样的 const 整型类型可以声明数组大小)
- 并不是所有常量表达式都是编译时求值, 由编译器决定何时求值
constexpr
constexpr 常量在编译时求值, 必须使用常量表达式初始化
- 会隐式具有
const 限定符
constexpr 函数可以在运行时求值, 但如果传入的参数是常量表达式, 则可以在编译时求值
- 在常量表达式中调用
constexpr 函数时, 必须编译时可计算
consteval 函数必须在编译时求值
constexpr 只能用 const 前向声明
constexpr 函数中使用 if consteval 感知编译时与运行时求值
- 实际上是强制编译时求值与非强制编译时求值 + 运行时求值
- 强制编译时求值:
if consteval 为真分支
- 非强制编译时求值 + 运行时求值:
if consteval 为假分支
隐式类型转换
- 情况
- 初始化或赋值
- 返回值类型与实际返回类型不匹配
- 函数参数类型与实参类型不匹配
- 具有不同类型操作数的某些二元运算符
- if 语句的条件表达式类型与布尔类型不匹配
- 转换的类型
- 限定符转换: 添加或移除
const 或 volatile 限定符
- 指针的类型转换
- 指针间的转换
- 空指针转换为成员指针 / 将基类的成员指针转换为派生类的成员指针
- 非抛异常的函数指针转换为函数指针
- 数字提升
- 整数提升: 小于
int 的整数类型提升为 int 或 unsigned int
- 浮点数提升:
float 提升为 double
- 数值转换
- 整数与浮点数间的转换
- 不同大小整数间的转换
- 不同大小的浮点数间的转换
- 将整数, 无作用域枚举, 指针或成员指针转换为布尔值
- 值类别转换
- 将左值表达式转换为右值表达式
- 数组退化
- 将函数转换为函数指针
- 将值转换为临时对象
窄化转换
- 定义
- 目标类型无法容纳源类型的所有值 (潜在不安全)
- 场景: 浮点转整型, 浮点精度降低, 整数无法精确表示 (宽转窄 / 符号转换)
constexpr 初始化式豁免
- 若源值为
constexpr 且精确存储在目标类型中, 不视为窄化转换
- 允许字面量初始化无需后缀 (
unsigned int u { 5 }; 无需 5u)
- 允许
constexpr 变量跨类型初始化
- 特例: 浮点转整型始终视为窄化, 即使
constexpr 且无损 (int n { 5.0 }; 错误)
- 特例:
constexpr 浮点转低精度浮点, 只要在范围内, 精度丢失也不视为窄化
强制类型转换
- C 风格强制转换
(new_type)expression 或 new_type(expression)
- 尝试按顺序执行
const_cast, static_cast, static_cast + const_cast, reinterpret_cast, reinterpret_cast + const_cast
- 不安全, 不推荐使用
static_cast<new_type>(expression)
- 用于相关类型间的转换 (类层次结构内的上行与下行转换, 数值类型间的转换)
- 不允许移除
const 或 volatile 限定符
- 不允许无关类型间的转换
dynamic_cast<new_type>(expression)
- 用于类层次结构内的安全下行转换
- 只能用于有虚函数的类
- 如果转换失败, 指针类型返回空指针, 引用类型抛出
std::bad_cast 异常
const_cast<new_type>(expression)
- 用于添加或移除
const 或 volatile 限定符
- 只能用于指针或引用类型
- 移除
const 后修改对象会导致 UB, 除非对象本来就不是常量
reinterpret_cast<new_type>(expression)
- 用于无关类型间的转换 (指针与整数间的转换, 不同指针类型间的转换)
- 不保证转换后指针的对齐要求
- 通常不可移植, 应谨慎使用
类型推导
auto
- 忽略顶层
const 和引用
- 如果初始化器是数组或函数, 则退化为指针类型
- 使用
auto 返回值类型的函数, 仅靠前向声明无法推导返回类型, 导致编译错误
- 最好别用
- 或者使用尾置返回类型语法
auto func(...) -> return_type (返回类型依赖于参数 / lambda 表达式必用)
复合类型
- 分类
- 函数 + 数组 + 指针 (对象 / 函数)
- 指向成员的指针 (数据成员 / 成员函数)
- 引用 (左值引用 / 右值引用)
- 枚举 (无作用域枚举 / 作用域枚举)
- 类 (结构体 / 联合体 / 类)
引用
- 引用遵循与普通变量相同的范围和持续时间规则
- 非
const 左值引用
- 只绑定到与其被引用类型匹配的对象, 因为类型转换的结果是右值
const 左值引用
- 可以绑定到右值, 这时会创建一个临时对象, 并延长其生命周期至引用的生命周期结束
- 类型也可以不匹配, 会进行隐式类型转换, 这时要注意, 引用的并不是原始对象, 其状态不会同步
- 从函数返回的临时对象不符合生命周期延长的条件
constexpr 左值引用
- 只能绑定到静态存储期对象, 因为它们的地址是编译时可知的
constexpr 引用不是默认的 const 引用
- 按引用传递还是按值传递
- 引用传递编译器会考虑指针别名, 导致无法内联优化
sizeof(T) <= 2 * sizeof(void*) 时按值传递通常更快 (忽略设置开销)
枚举
- 枚举类型指类型本身, 枚举的特定值称为枚举器
- 枚举器是隐式
constexpr
- 零初始化会被初始化为 \(0\), 即使该值不在枚举器列表中
- 无作用域枚举指枚举器在包含枚举的作用域内可见
red 与 Color::red 等价
- 隐式转换为整数类型 (可以指定
enum MyEnum : char { ... };)
- 有作用域枚举指枚举器在枚举作用域内可见 (
enum class 或 enum struct)
- 必须使用
Color::red
- 不隐式转换为整数类型,
std::to_underlying() 可显式转换为整数类型
using enum MyEnum; 导入枚举器到当前作用域
类类型
- 类类型都可以模板化
- 类内部定义的成员函数是隐式内联的
const 成员函数不会修改对象状态, 可以在 const 对象上调用
- 访问级别是基于类的, 不是基于对象的
- 即使是
private 成员, 也可以在该类的其他对象上访问
- 可以定义成员类型, 比如 STL 容器的迭代器类型
- 静态成员变量本质上就是类作用域的全局变量
- 静态成员变量必须在类外定义
- 除非它是
const / inline, 可以在类内给出初始化器 (初始化器中放一个静态成员函数就可以初始化时实现逻辑)
- 但相应的, 得到可以使用类型推导的权利
- 友元函数可以在类内定义
引用限定符重载
- 当调用成员函数的对象是隐式对象时, 它不一定是左值 / 右值
- 一旦重载了引用限定符, 不允许给出无引用限定符的版本
const 的左值引用限定符可以由右值对象调用 (前提是没给出右值版本)
- 当然, 可以使用
=delete 删除不想要的重载
class My_class {
public:
const Data& func() & {return data;} // 只能在左值对象上调用
Data func() && {return std::move(data);} // 只能在右值对象上调用, 通常移动语义
const Data& func() const & {return data;} // 只能在 const 左值对象上调用
Data func() const && {return data;} // 只能在 const 右值对象上调用, 少见
};
构造函数
- 不妨使用
static constexpr 成员变量作为其它成员的默认初始化器
- 默认构造函数相当于
My_class(){}
- 但是默认构造对象会先进行零初始化, 然后调用默认构造函数
My_class(){} 会直接调用默认构造函数, 不会零初始化
- 合成
- 只要提供了一个构造函数, 编译器就不会合成默认构造函数
- 只要没提供拷贝构造或赋值运算符, 编译器就会合成它们
- 提供了拷贝构造或赋值运算符, 编译器就不会合成对应移动构造或移动赋值运算符 (析构也会)
- 当包含
const 成员变量时, 赋值运算符默认被删除
explicit
- 不能用于拷贝初始化 / 拷贝列表初始化
- 不能用于隐式转换
聚合
- C 风格数组 + 无构造无非
pubilc 成员无虚函数的类类型为聚合类型
- 使用
{} 初始化器列表初始化聚合类型, 成员按声明顺序初始化
- 也可以
{.member1 = value1, .member2 = value2} 指定成员初始化
- 初始化器缺失且存在默认成员初始化器使用默认值, 否则值初始化
constexpr 类与成员函数
constexpr 构造函数允许类与聚合一样使用 constexpr 初始化
- C++ 14
constexpr 成员函数不再隐式 const
析构函数
- 默认是
noexcept(true) (能抛, 但是会自动调用 std::terminate() 终止程序)
- 因为有可能某个函数抛出异常, 导致栈展开 (销毁局部变量), 触发析构函数时出现双重异常
- 如果析构函数可能抛出异常, 应显式声明为
noexcept(false), 并且在析构函数内处理所有异常
继承
- 继承类型用于改变可访问的基类成员的访问级别
- 如何访问基类的被覆盖的成员函数
- 直接
using Base_class::func; 引入基类版本 (? 不是说覆盖了吗 -> 重载)
- 使用作用域解析运算符:
Base_class::func();
- 对于非成员函数, 使用类型转换
using 声明引入基类的成员函数 -> 可以修改访问级别 (会引入所有重载版本)
obj.Base_class::member 指定访问基类的成员
- 协变返回类型
- 派生类重写基类的虚函数时, 返回类型可以是基类返回类型的派生类
- 仅适用于指针与引用类型
- 纯虚函数
dynamic_cast 用于类层次结构内的安全下行转换 (安全的 static_cast)
- 使用 RTTI (运行时类型信息), 空间开销大 (有虚函数的类才有 RTTI, 放在虚函数表中,
typeid (返回 type_info 对象) 也需要 RTTI)
- 只能用于有虚函数的类, 并且必须是
public 继承的类
- 如果转换失败, 指针类型返回空指针, 引用类型抛出
std::bad_cast 异常
模板
auto 函数参数的函数实际上是模板函数, 每个参数都是独立的模板参数
- 模板别名,
template<typename T> using My_type = ...;
- 完全特化不是隐式内联的
extern template class My_class<int>; 显式实例化声明, 防止在该翻译单元实例化
变量模板
template<typename T>
constexpr T pi = T(3.1415926);
double circumference(double r) {
return 2 * pi<double> * r;
}
函数模板
- 函数模板实例化
- 编译器在每次调用函数模板时根据实参类型生成对应的函数定义
- 如果多个调用使用相同的模板参数类型, 则只实例化一次
foo<int>(1) 显式指定模板参数类型
foo(1) 或 foo<>(1) 隐式推导模板参数类型
auto foo(T a, U b) -> std::common_type_t<T, U> 使用 std::common_type_t 获取通用类型
- 函数模板也可以重载
偏特化
- 类模板可以偏特化, 函数模板不行
template<typename T> class My_class<T*> { ... }; 指针偏特化
模板参数
- 非类型模板参数
- 整型, 枚举类型, 对象 + 函数 + 成员函数指针与引用
- 浮点类型和字面量类类型
- 空指针
- 可以使用
auto
- 模板参数可以有默认值
- CTAD: 类模板参数推导
- 编译器根据构造函数参数推导模板参数类型
My_class<int> obj1; 可简写为 My_class obj1{...};
- 独属于 C++ 17 聚合类型类模板需要推导指南 (非聚合类型的构造函数起到相同作用)
- 在类类型的非静态成员初始化中无法使用 CTAD
语句与函数
语句
- 表达式
- 操作符和操作数的组合, 除调用
void 函数外必有返回值, 可能有副作用
- 三元运算符
? : 的返回类型是第二和第三操作数的共同可转换类型, 并且这两个操作数必须可以转换为同类型, 或者是个 throw 表达式
- 重载的逻辑运算符 (
&& 和 ||) 不会短路求值
- C++ 17 之前连结合性都是错的, 因为函数调用的参数不规定求值顺序
if (!!a != !!b != !!c) // a XOR b XOR c
- 基本语句
- 声明语句
- 表达式语句: 表达式后跟分号
- 复合语句
- 空语句
- 控制流语句
- try-catch 语句
表达式
- 表达式就是值
- 一个求值为可识别对象或函数的表达式称为左值表达式 (可修改 / 不可修改)
- 否则称为右值表达式
Constexpr if 语句
if constexpr (condition) { ... } else { ... }
Switch 语句
[[fallthrough]] 属性 + 空语句用于标记有意的贯穿行为, 避免编译器警告
- 可以在块中声明与定义变量, 作用域为该块, 但是不允许初始化
函数
- 无返回值函数无需在结尾使用
return
- 未命名参数
- 用于向后兼容
func(int /* 但是你应说明清楚 */);
- 用于在运算符重载时区分
++ 和 -- 的前置与后置版本
- 需要模板参数的类型信息做决策, 但不需要参数值
- 编译器会自己决定内联优化, 部分情况下无法内联 (另一个翻译单元的函数)
- 现在的内联指允许在不同翻译单元中具有重复定义, 只要每个定义都相同 (否则未定义行为)
- 内联函数
- 内联函数的定义必须出现在每个使用它们的翻译单元中
- 成员函数,
consteval 函数和 constexpr 函数, 从函数模板隐式实例化的函数隐式为内联函数
- 内联变量
- C++ 17 之前常在头文件中定义一个命名空间, 内部若干
constexpr 变量
- 或者索性外部链接 + 前向声明, 但影响
constexpr 变量的使用
- 内联变量的定义必须出现在每个使用它们的翻译单元中
- 内联变量默认具有外部链接
- 静态
constexpr 数据成员隐式为内联变量
- 默认参数
- 由编译器在函数调用点插入 (因此函数指针不包含默认参数信息)
- 不能重复声明 (没错, 重复声明都不行) + 必须使用前给出 -> 只能在函数声明中指定默认参数
- 调用时参数必须从左到右提供, 导致你不能使用靠前的默认参数而指定靠后的默认参数
函数重载
- 区分参数类型与参数数量, 不区分返回类型
- 忽略别名
- 忽略值参数上的
const
- 包括
... 参数
- 对于成员函数, 还区分
const, volatile 以及引用限定符
- 重载决议
- 寻找最佳匹配
- 精确匹配: 无转换, 左值转右值, 限定符转换, 非引用到引用
- 数值提升
- 数值转换: 例如对于
char, double 先于 std::string
- 用户定义转换
- 使用
... 参数
- 不可转换
- 对于多参数函数, 比较每个参数的转换等级, 要求所有参数都不劣于另一个函数的对应参数, 且至少有一个参数优于另一个函数的对应参数, 否则二义性错误
- 如果多个函数同为最佳匹配, 则重载决议失败 (二义性错误)
- 如果没有函数可调用, 则重载决议失败 (无匹配错误)
- 对于不想要的重载, 可以删除它们:
void func(int) = delete;
- 使用模板
func(T) = delete; 删除所有其他类型的重载
运算符重载
- 使用成员函数形式的运算符重载时, 左操作数必须是该类的对象
= / [] / () / -> 必须使用成员函数形式重载
- 只需要
== 与 < 就可以表达所有比较运算符
- 重载
<=> 可以自动生成除了 == 的所有比较运算符, 但如果使用默认的 <=> 生成器, 则 == 也会被自动生成
- 三路运算符的结果类型为
std::strong_ordering / std::weak_ordering / std::partial_ordering, 就是强序 / 弱序 / 偏序
- 类似差值, 再与零比较就能得到序
- 通过一个
int 参数区分前置与后置 ++ / --
- 通常前置返回引用, 后置返回值 (需要保存旧值, 会有额外开销)
operator int() 重载类型转换运算符, 允许对象隐式转换为 int
- 可能导致意外的隐式转换, 可以使用
explicit 关键字禁止隐式转换
- 必须是非
static 成员函数, 没有参数, 一般是 const 成员函数
// 类型推导 + 显示 this 指针 -> 一次生成 const 与非 const 版本
auto&& operator[](this auto&& self, int index)
{
return self.m_list[index];
}
Lambda 表达式
- 基本语法 ->
[/* 捕获列表 */](/* 可选参数列表 */) /* 可选限定符 */ /* 可选返回类型 (用 -> type 表达) */ { /* 函数体 */ }
- 事实上是一个匿名类的对象, 捕获列表中的变量作为该类的成员变量, 重载
operator()
- 因此 Lambda 表达式中的
static 变量对于每个副本都是一致的
- 空捕获列表的 Lambda 可赋值给函数指针,
std::function 也可以
- Lambda 的参数列表中也可以使用
auto, 行为类似函数
- C++ 17 开始, 能
constexpr 的 Lambda 表达式隐式为 constexpr
- Lambda 捕获
- 捕获的变量相当于成员变量, 与 Lambda 表达式生命周期一致
- 可以直接使用静态存储期变量 +
constexpr 变量
- 捕获默认是
const 的, 除非使用 mutable 关键字
[foo] 表示按值捕获, 获得的是该变量的一个副本 (类型不一定相同, 比如数组退化为指针)
[&foo] 表示按引用捕获, 获得的是该变量的一个引用
[=] 表示按值捕获所有用到的但没显示捕获的变量
[&] 表示按引用捕获所有用到的但没显示捕获的变量
[foo {foo1 + foo2}] 可以定义一个 Lambda 内部使用的变量
- 通过
std::reference_wrapper / std::function 抑制可变 Lmabda 表达式的复制导致的状态分裂
参数包与折叠表达式
- 参数包用以替代 C 语言的
std::arg_list
template<typename... Args> // Args 是一个模板参数包
void func(Args... args){ // args 是一个函数参数包
// 展开
func1(args...); // 展开参数包 -> func1(arg1, arg2, arg3, ...)
func2(&args...); // 展开参数包 -> func2(&arg1, &arg2, &arg3, ...)
func3(h(args)...); // 展开参数包 -> func3(h(arg1), h(arg2), h(arg3), ...)
const size_t n = sizeof...(Args); // 获取参数包的大小
}
- 无法预料到参数包中会有多少个参数, C++17 之前只能通过递归或逗号表达式技巧来处理参数包, 代码冗长且可读性差
// 递归
template<typename T, typename... Args>
T sum(T first, Args... rest) {
return first + sum(rest...);
}
// 逗号表达式技巧
template<typename... Args>
void print_all(Args... args) {
int dummy[] = { 0, (std::cout << args << " ", 0)... };
}
折叠表达式
- E 是一个表达式, 包含一个参数包展开的占位符
..., 展开针对参数包中的每个参数进行, 形成新的表达式
| 类型 |
语法 |
展开逻辑 |
| 一元右折叠 (Unary Right) |
(E op ...) |
(a op (b op c)) |
| 一元左折叠 (Unary Left) |
(... op E) |
((a op b) op c) |
| 二元右折叠 (Binary Right) |
(E op ... op Init) |
(a op (b op (c op Init))) |
| 二元左折叠 (Binary Left) |
(Init op ... op E) |
(((Init op a) op b) op c) |
- 加法没有默认值, 建议
(0 + ... + args); // 空包返回 0, 否则空包会导致编译错误
- 经常结合完美转发使用
- Lambda 捕获参数包的语法糖:
[...args = std::move(args)] (C++20)
- auto 参数包的语法糖:
void func(auto... args) (C++20), 这实际上是一个函数模板, 每个参数都是独立的模板参数, 但无法直接访问这些模板参数, 只能通过 decltype 获取参数类型
移动语义
- 类型支持 + 用右值进行初始化或赋值时触发移动语义就会触发移动语义 (
std::move 只是强制转换为右值)
std::move 之后对象进入有效但未指定状态, 只能销毁或赋值, 不应访问 (除非赋值后访问)
- 本质上是用
static_cast 将左值转换为右值, 但并不真的移动任何东西
std::move_if_noexcept 只有在移动构造函数声明为 noexcept 或者没有移动构造函数时才会返回右值, 否则返回左值
- 适用于容器的元素类型, 因为容器在扩容时会使用移动构造函数, 如果移动构造函数可能抛出异常, 则会退化为拷贝构造函数, 导致性能下降
范围 for 语句
- 适用于任何提供
begin() 和 end() 函数的类型 (迭代器需要支持 != ++ 和 *)
- 全局的
begin(container) 和 end(container) 函数优先于成员函数
- 最佳实践:
for (const auto& word : words
- 反向:
for (const auto& word : std::views::reverse(words)) C++ 20 Ranges
异常
- 异常处理机制
try 块内的异常通过 throw 抛出
- 通过
catch 块捕获异常
- 会展开栈, 销毁所有局部对象
- 会按顺序检查
catch 块, 直到找到匹配的类型
- 可以有多个
catch 块, 按捕获类型匹配 (类型不会发生转换, 但是基类可以捕获派生类, void* 可以捕获任何指针类型)
- 未捕获的异常会调用
std::terminate() 终止程序 (此时栈不一定会展开)
catch (...) 捕获所有类型的异常
- 自定义异常类型应使用
const T& 捕获, 以避免不必要的复制并保留多态 (基类型引用可以捕获派生类型的对象)
- 异常对象需要是可复制的 (栈展开时会复制异常对象)
- 只记录不解决异常
- 在
catch 块内记录异常信息, 然后重新抛出异常 (throw;)
自定义异常类型
- 继承自
std::exception (\<exception>) 或 std::runtime_error (\<stdexcept>)
- 重写
what() 成员函数返回异常描述字符串 (注意 noexcept)
函数 try 块
- 构造函数的
try 块必须继续 throw 异常, 不允许 return, 到达结尾会隐式 throw
- 析构函数块可以使用
return 等结束语句, 到达结尾会隐式 throw
- 其它函数的
try 块可以使用 return 等结束语句, 到达结尾会隐式解决返回值为 void 的函数的异常, 对于非 void 函数未定义行为
class My_class {
public:
My_class() try : member1(), member2() {
// 构造函数体
} catch (...) {
// 处理构造成员时抛出的异常
}
};
异常规范
noexcept 规范
noexcept(true) 表示函数不会抛出异常, 如果抛出异常会调用 std::terminate()
noexcept(false) 表示函数可能抛出异常 -> 常用于模板根据条件选择是否可抛出异常
- 编译器合成的构造函数和赋值运算符 + 比较运算符不抛出异常
- 析构函数默认
noexcept(true)
- 另外,
noexcept 可以当运算符使用, 返回一个 constexpr 布尔值, 表示表达式是否为 noexcept