跳转至

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_tuint8_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)
  • 列表初始化
    • 基本行为类似直接初始化
  • 值初始化
    • 基本类型触发零初始化
    • 类类型调用默认构造函数, 没有默认构造函数则递归触发零初始化, 再调用编译器合成的默认构造函数

类型限定符: constvolatile

  • const
    • 对于基本类型, 返回类型上的 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 语句的条件表达式类型与布尔类型不匹配
  • 转换的类型
    • 限定符转换: 添加或移除 constvolatile 限定符
    • 指针的类型转换
      • 指针间的转换
      • 空指针转换为成员指针 / 将基类的成员指针转换为派生类的成员指针
      • 非抛异常的函数指针转换为函数指针
  • 数字提升
    • 整数提升: 小于 int 的整数类型提升为 intunsigned int
    • 浮点数提升: float 提升为 double
  • 数值转换
    • 整数与浮点数间的转换
    • 不同大小整数间的转换
    • 不同大小的浮点数间的转换
    • 将整数, 无作用域枚举, 指针或成员指针转换为布尔值
  • 值类别转换
    • 将左值表达式转换为右值表达式
    • 数组退化
    • 将函数转换为函数指针
    • 将值转换为临时对象

窄化转换

  • 定义
    • 目标类型无法容纳源类型的所有值 (潜在不安全)
    • 场景: 浮点转整型, 浮点精度降低, 整数无法精确表示 (宽转窄 / 符号转换)
  • constexpr 初始化式豁免
    • 若源值为 constexpr 且精确存储在目标类型中, 不视为窄化转换
    • 允许字面量初始化无需后缀 (unsigned int u { 5 }; 无需 5u)
    • 允许 constexpr 变量跨类型初始化
    • 特例: 浮点转整型始终视为窄化, 即使 constexpr 且无损 (int n { 5.0 }; 错误)
    • 特例: constexpr 浮点转低精度浮点, 只要在范围内, 精度丢失也不视为窄化

强制类型转换

  • C 风格强制转换
    • (new_type)expressionnew_type(expression)
    • 尝试按顺序执行 const_cast, static_cast, static_cast + const_cast, reinterpret_cast, reinterpret_cast + const_cast
    • 不安全, 不推荐使用
  • static_cast<new_type>(expression)
    • 用于相关类型间的转换 (类层次结构内的上行与下行转换, 数值类型间的转换)
    • 不允许移除 constvolatile 限定符
    • 不允许无关类型间的转换
  • dynamic_cast<new_type>(expression)
    • 用于类层次结构内的安全下行转换
    • 只能用于有虚函数的类
    • 如果转换失败, 指针类型返回空指针, 引用类型抛出 std::bad_cast 异常
  • const_cast<new_type>(expression)
    • 用于添加或移除 constvolatile 限定符
    • 只能用于指针或引用类型
    • 移除 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\), 即使该值不在枚举器列表中
    • 最好使 \(0\) 成为一个枚举器
  • 无作用域枚举指枚举器在包含枚举的作用域内可见
    • redColor::red 等价
    • 隐式转换为整数类型 (可以指定 enum MyEnum : char { ... };)
  • 有作用域枚举指枚举器在枚举作用域内可见 (enum classenum 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 指定访问基类的成员
  • 协变返回类型
    • 派生类重写基类的虚函数时, 返回类型可以是基类返回类型的派生类
    • 仅适用于指针与引用类型
  • 纯虚函数
    • 声明时赋值为 0
  • 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) 隐式推导模板参数类型
    • 重载情况下, 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 { ... }
    • condition 必须为常量表达式

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 也可以
    • 但最佳选择是 auto
  • 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