Modern C++(11~26)笔记

堆、栈、RAII、容器

  • delete 一个空指针是合法的,但不能多次 delete
  • 手写智能指针:先实现一个 auto_ptr(赋值为移动语义),再定义引用计数类;每个智能指针类包含两个类的指针:引用技术类和对象类;
  • 异常安全的赋值函数:复制并交换;
1
2
3
4
T& operator= (T rhs) {
rhs.swap(*this);
return *this;
}
  • vector 保证强异常安全性,如果没有 noexcept 的移动构造函数,会调用拷贝构造函数;
  • queuestack 依赖现有容器,因此称为容器适配器,默认均为 deque 实现;
  • lessgreater 均为通过重载同名结构体的 bool operator() (const T& x, const T& y) 得到的函数对象;它们继承的 binary_functionunary_function 已在 C++11 被废弃;

右值引用和移动语义

  • 生命周期延长:若一个 prvalue 被绑定到一个引用上,其生命周期会被延长至引用变量的生命周期;
  • C++11 后的移动语义使 string str = string("Hello") + name + "." 这样的语句调用 operator+(const string&&, const string& / const char*) 从而减少了拷贝次数;
  • 实现五个特殊成员函数时应尽量带上 noexcept
  • 右值引用类型的变量本身是左值;
  • 返回值优化 NRVO:在一些简单条件下会直接在栈上构造对象,从而减少拷贝次数;
  • xvalue 是一个可以被移动的值,并且与某个存储位置关联(如通过 std::move 生成的已命名对象);prvalue 是一个纯粹的值,不与任何存储位置关联(如一个整数字面值或一个计算的结果);glvalue (generalized lvalue) 包括 lvalue 和 xvalue;rvalue: 包括 xvalue 和 prvalue。
  • decltype<auto> 推导规则:
    • prvalue(例如临时变量)推导出 type
    • lvalue(例如有名字的对象)推导出 type&
    • xvalue(例如用 std::move()标记的对象)推导出 type&&

异常

  • 声明了 noexcept 的函数抛出异常会直接导致 std::terminate
  • 特殊成员函数若内部调用的函数均为 noexcept ,则本身也为 noexcept
  • 异常安全的四个等级:
    • 无异常安全:抛出异常会进入未定义状态;
    • 基本异常安全:抛出异常保证处于一致状态,但不保证保持原始状态(有副作用);
    • 强异常安全:抛出异常将回滚;
    • 不抛异常安全:不抛异常;

其它

  • 一个迭代器必然支持 ++* ,若支持 * 输出则为输出迭代器;若支持 * 读取则为输入迭代器,再可反复读取则为前向迭代器,再支持 -- 则为双向迭代器,再支持跳跃和比较则为随机访问迭代器,若还保证对象在内存连续存放则为连续迭代器(C++20);
  • C++11 会尽量使用移动返回对象,C++17 允许返回不可拷贝、不可移动的对象:直接构造于目标位置;新标准之后,应尽量使用返回值而非参数返回对象;
  • C++11 引入了 char16_tchar32_t ,分别代表 UTF-16 和 UTF-32;C++20 引入了用于存储 UTF-8 的 char8_t

自动类型推断、字面量、静态断言

  • C++17 之后 array 可以不带 <int, 3> 而使用自动推断了;也允许使用 auto [lower, upper] = Map.equal_range("four");
  • C++11 允许使用大括号初始化列表在成员变量定义时默认初始化;
  • C++11 允许使用 operator "" 定义下划线开头的字面量;
  • C++14 支持 0b 表示二进制字面量,支持在数字中添加 ' 使其更可读;
  • C++11 提供静态断言 static_assert(condition, output_info) ,可以直接在类定义中使用;

编译期多态、泛型编程与模板

  • 模板元编程提供图灵完备的类型推导;
  • 使用静态成员变量储存 value, typedef 储存 type;
  • 使用替换失败非错(SFINAE)和 enable_if 实现条件编译;
  • declval<T> 为类型 T 创建一个假设的右值引用,而不实际构造该类型的对象;它只有声明因此只能用于编译时上下文;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// integral_constant
template <class T, T v>
struct integral_constant {
static constexpr T value = v;
typedef T value_type;
typedef integral_constant type;
constexpr operator value_type() const noexcept { return value; }
constexpr value_type operator()() const noexcept { return value; }
};

typedef std::integral_constant<bool, true> true_type;
typedef std::integral_constant<bool, false> false_type;

// enable_if:仅当 B 为 true 时拥有 type;
template<bool B, class T = void>
struct enable_if {};

template<class T>
struct enable_if<true, T> { typedef T type; };

// IsIntegral
template <typename T, typename Enable = void>
struct IsIntegral : std::false_type {};

template <typename T>
struct IsIntegral<T, std::enable_if_t<std::is_integral<T>::value>> : std::true_type {};

template <typename T, typename = typename std::enable_if<std::is_integral<T>::value>::type>
void foo_integral(T value) {}

template <typename T, typename = typename std::enable_if<!std::is_integral<T>::value>::type>
void foo_not_integral(T value) {}

// has_reserve
template <typename T, typename = void>
struct has_reserve : std::false_type {};

template <typename T>
struct has_reserve<T, typename std::enable_if<true, decltype(std::declval<T>().reserve(1))>::type>
: std::true_type {};

函数式编程、可变模板和 **tuple** 的编译期技巧

  • [this] 按引用捕获外围对象, [*this] 按值捕获,调用拷贝(C++17);
  • lambda 表达式可以取代 bind
  • Map 在 C++ 中对应 transform ,Reduce 在 C++ 中对应 accumulate (C++17 已有 reduce() ,要求归并操作的交换律和结合律,Filter 在 C++ 中对应 copy_ifpartition
  • 实现 Compose:
1
2
3
4
5
6
template <typename F, typename... Args>
auto compose(F f, Args... other) {
return [f, other...](auto&&... x) {
return f(compose(other...)(forward<decltype(x)>(x)...));
};
}
  • 简化版 std::integer_sequenceindex_sequence (C++14):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
template <class T, T... Ints>
struct integer_sequence {};

template <size_t... Ints>
using index_sequence = integer_sequence<size_t, Ints...>;

template <size_t N, size_t... Ints>
struct index_sequence_helper {
typedef typename index_sequence_helper< N - 1, N - 1, Ints...>::type type;
};

template <size_t... Ints>
struct index_sequence_helper<0, Ints...> {
typedef index_sequence<Ints...> type;
};

template <size_t N>
using make_index_sequence = typename index_sequence_helper<N>::type;

template <class F, class Tuple, size_t... I>
constexpr decltype(auto) apply_impl(F&& f, Tuple&& t, index_sequence<I...>) {
return f(get<I>(forward<Tuple>(t))...);
}

template <class F, class Tuple>
constexpr decltype(auto) apply(F&& f, Tuple&& t) {
return apply_impl(forward<F>(f), forward<Tuple>(t),
make_index_sequence<tuple_size_v<remove_reference_t<Tuple>>>{}
);
}

C++14 新特性

C++11,14,17,20 特性总结(英文)

  • 放宽 constexpr 限制;
  • 支持变量模板( constexpr T pi = T(3.14) )、别名模板(泛型关联:给模板定义类型起别名)与变参模板(参数个数可变);
  • 支持 auto 参数的泛型 lambda、lambda 初始化捕获;
  • 支持返回类型推导、 decltype<auto>
  • 聚合初始化(用初始化列表初始化结构体)、 make_unique()
  • 二进制字面量 0b110 、整型字面量分隔符 12'345
  • 透明比较扩展:支持 std::setstd::map 两个比较数不是同一类型,从而可以用 const char* 查找 std::set<std::string>
  • std::shared_timed_mutexstd::shared_lock 等;
  • [[deprecated]]std::get 获取元组元素、 使用移动语义的std::exchange

C++17 新特性

C++17 特性总结

  • 结构化绑定
    • 可以使用 auto [u, v] = myStructauto [u, v] {myStruct}auto [u, v] (myStruct) 解构对象;也可以用在 for (const auto& [key, val] : myMap) 中;
    • 本质上是匿名本地拷贝了 myStruct (假设为 e ), uv 分别是 e 的成员,若使用 auto& ,则 e 为原对象引用,即修饰符并非作用于 uv
    • 结构化绑定同样也适用于原生数组、 std::arraystd::pairstd::tuple 不会产生类型退化;
  • 属性
    • [[nodiscard]] :鼓励编译期在返回值未被使用时给警告;
    • [[maybe_unused]] :避免编译期在返回值未被使用时给警告;
    • [[fallthrough]] :避免编译期在 switch 语句某个标签缺少 break 时给警告;
    • [[deprecated]] :弃用;
  • 带初始化的 ifswitch 语句:类似 for ,可以使用 auto ,定义变量仅在此语句中有效;
  • 在头文件中使用 inline static 定义 全局变量(必须直接初始化,且必须是字面类型);现在 static constexpr 修饰符隐含 inline
  • 聚合体可以拥有基类;C++17 中数组和满足一定条件的 类类型classstructunion )均被认为是 聚合体
  • 强制省略拷贝或传递未实质化(从 prvalue 转化为 xvalue)的对象:比具名返回值优化(NRVO)更强,可以在 delete 禁止拷贝和移动函数的情况下工作;
  • 进一步放宽 lambda 和 constexpr 范围,支持 constexpr lambda;
  • 嵌套命名空间;
  • 解决了部分表达式求值顺序无保证的问题;
  • C++17 之后直接初始化 auto a{42} 会得到 int ,拷贝初始化 auto a = {42} 会得到 initializer_list
  • u8'6' 表示 UTF-8 字符字面量;
  • 异常声明将作为函数类型的一部分;
  • 自动类模板参数推导;支持 std::pair p(a, b) 而不需 make_pair
  • 使用编译期 if 语句 if constexpr(...) 在编译期决定生成 then 还是 else 的代码;
  • 折叠表达式:有一些处理空参数包的规则;
1
2
3
4
5
6
7
return (... + args);
return (args + ... + 0);
(..., foo(forward<T>args));

static Node* traverse(T np, TP... paths) {
return (np ‐>* ... ‐>* paths); // np ‐>* paths1 ‐>* paths2
}
  • 对模板能力的进一步扩展,可以使用 autodecltype<auto> 作模板参数;
  • 类型擦出容器和代数数据类型:
    • std::optional<T> 在空时为 std::nullopt
    • std::variant<int, double, std::string> ,更安全的 union
    • std::any 不需提前声明存储哪些类型,在取值时才通过模板参数指定,更安全的 void*
    • std::anystd::optionalstd::variant 均为值语义,赋值时执行深拷贝;
  • 字符串视图 std::string_view :一段字符串的引用,不具有所有权,具备 std::string 所有只读接口;
  • std::apply(func, std::tuple(...)) 将容器值作为函数输入;
  • C++17 采纳了 Boost.Filesystem 提供文件系统库,在 std::filesystem 下;
  • 支持类型特征后缀:可用 _v 代替 ::value
  • 支持使用 std::execution::par 调用并行 STL 能力,需要 tbb 库;
  • 新增泛型辅助函数 size()emptydata()
  • clamp() 找到三个值中居中的那个; sample() 获取一个随机子集;
  • emplace() 系函数现在返回新插入对象的引用; try_emplace({key, std::move(value)}) 在插入成功时才会移动; insert_or_assign() 无则插入有则替换,必然移动;
  • std::vectorstd::liststd::forward_list 开始支持不完全类型(可以在一个类中声明本身类型的 vector 作为成员变量);
  • 提供 std::scoped_lock<> 同时锁住多个互斥量,std::shared_mutex<> 支持读写锁,是 C++14 的 std::shared_timed_mutex<> 的子集;
  • m.extract() 提取一个容器(可以用迭代器或键值指定),返回一个 std::map<Key, T>::node_type ,此时 m 不再包含此值;
  • <numeric> 中新增 std::gcd()std::lcm()
  • 大量新增算法 API、专家特性与辅助特性;
  • 数字与字符串互转:C++11 有 std::to_string()std::stoi() ,C++17 新增 std::from_chars()std::to_chars()
  • C++14 兼容 C99,而 C++17 兼容 C11;
  • C++11 废除空异常声明 throw() ,C++17 废除动态异常声明 throw(std::bad_alloc)
  • register C++17 起不再有语义,但关键字仍然保留;
  • 禁止 bool 使用 ++ ,移除 auto_ptr ;移除 random_shuffle() ,使用 std::shuffle() 代替;
  • 移除 std::unary_functionstd::binary_function

C++20 新特性

C++20 特性总结

  • 格式化输出:
1
2
format("Hello {1} {0}", "second", "first"// Hello first second
format("{:.5}, {:.<5}", 3.14159, 42); // 3.1416, 42...
  • 进一步放宽 constexpr 范围;

  • <utility> 使用 cmp_less() 等比较两个不同类型的数;

  • “宇宙飞船” <=>

    • 返回一个 <compare> 中的对象,可以与 0 比较;若小于则返回值小于 0,大于则大于 0,等于则等于 0;
    • 若操作数为整型,则返回 strong_ordering::equal/less/greater ;若为浮点型,返回 partial_ordering::equivalent/less/greater/unordered
    • <=> 重载
    • C++20 后会将 a > b 重写为 b <=> a < 0 ,因此可以直接通过重载 auto operator<=>(const Num&) const = default; 省去列举每个比较操作符的重载(默认按定义顺序依次将每个变量作为关键字),而只需再重载 ==
  • 协程(Coroutines):

    • 无栈协程,使用 co_returnco_awaitco_yield

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // sample 1
    generator<int> range(int start, int end) {
    while (start < end) {
    co_yield start;
    start++;
    }

    // co_return;
    }

    for (int n : range(0, 10)) {...}

    // sample 2
    task<void> echo(socket s) {
    while (1) {
    auto data = co_await s.async_read();
    co_await async_write(s, data);
    }

    // co_return;
    }

  • 概念(Concepts):

    • requires Numeric<T> 约束(任意)模板,其中 concept Numeric = integral<T> || floating_point<T>; ;运算数结果为 bool ,结果需要为 true
    • 可以直接写 template <Numeric T> ,或写入参数 auto func(Numeric auto &arg);
    • 可以在函数、 lambda 中使用;三种约束的逻辑操作符:合取式(conjunctions)、析取式(disjunctions)、原子约束(atomic constraints),使用短路;
    • 以下代码可以一定程度代替虚函数,提供更好的 ABI 稳定性:

    1
    2
    template
    concept DerivedOfBaseClass = std::is_base_of_v<BaseClass, T>;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    template<typename T>
    concept Histogram = requires(T h1, T h2) {
    h1.getMoments(); // 要求有getMoments接口
    T::count; // 要求有静态变量count
    h1.moments; // 要求有成员变量moments
    h1 + h2; // 要求对象能够进行+操作

    typename T::type; // 要求存在类型成员type
    typename std::vector<T>; // 要求能够模板实例化并与std::vector组合使用

    { h1.getSubHistogram() } -> same_as<T>; // 要求接口返回类型与T一致
    { h1.getUnit() } -> convertible_to<float>; // 要求接口返回类型能转换成float,本质上接口返回类型可能是double
    { h1 = std::move(h2) } noexcept; // 要求表达式不能抛出异常

    requires sizeof(T) > 4;
    };

  • 模块(Modules):

    • 一个编译单元默认为普通单元,使用 module 则为模块单元;使用 export module my_module 的称为模块接口单元(对应传统头文件),使用 module my_module 称为模块实现单元(对应传统实现文件);使用 import my_module 导入模块;
    • 需要在函数签名、命名空间等前使用 export 对外公开;使用 export import one_module 重导出模块(类似 Rust 中的 pub use );
    • 若模块单元需要 #include<> ,或其它全局代码段,则需要在开头写一句 module; 引入全局模块,然后写预处理指令,写完后立即声明一个标准模块 export module my_module
    • 实现部分和接口部分可以放在同一个文件中,接口部分前使用 export module my_module ,实现部分前使用 module: private
    • 模块分区: module A:B; ,导入时只需要指定分区名称: export import :A; ;模块分区的所有声明均对模块本身公开, export 使其可以对模块外公开,但最终还需要主模块接口单元决定,即模块分区单元中的符号必须通过主模块的接口单元使用 export 控制对外可见性;
    • 可以使用 A.B 式模块名,但与 A 之间并没有父子模块关系;
    • Module 并没有解决符号冲突问题,仍然需要与 namespace 配合使用,二者保持正交设计;同样不能解决二进制分发与 ABI 对齐问题;
  • 范围(Ranges):

    • Range 是一个 Concept,要求是可迭代对象的集合,且支持 begin()end() 迭代器;
    • 视图是惰性迭代操作,从底层返回而不拥有任何数据,复杂度为 O(1);
    • 使用 ranges::take_view(ranges, n) 返回前 n 个元素的视图,支持只读操作,同时支持 for (int i: tv)
    • 视图适配器对函数式编程的支持:视图管道 auto ****result = nums | views::take(5) | views::reverse; 获取 nums 前五个元素的翻转;
    • nums | views::filter(lambda)nums | views::transform(lambda)views::reverse()views::iota(1, 10) 返回一系列递增值;
    • stackqueue 没有 begin()end()
    • ranges::sort(vec)views::drop(view)
  • std::span :C 数组的 string_view ;直接用 std::span<T> 传参将 C 数组升级为 span ,从而传递其长度,且支持 begin()front()empty() 等操作;注意它仍不检查越界;

1
2
auto remove_it = std::remove(c.begin(), c.end(), v); // 返回后面向前移动后的末尾下一个
c.erase(remove_it, c.end()); // 真正的删除
  • 使用 [[likely]][[unlikely]] 标记某个分支走到的概率;
  • 不建议隐式捕获 [=] 捕获 this ,已弃用;使用 [=, this] 显式捕获;
  • lambda 中可以使用模板语法,可以捕获参数包; [&...args = std::forward<Args>(args)] {...}
  • <bit> 头提供位操作; <numbers> 头中定义一些数学常量;
  • std::make_shared<int[]>(5)std::make_shared<int[5]>() 支持数组;
  • str.starts_with("foo")ends_with()mapset 新增 contains(key)
  • std::midpoint(1, 3) == 2 不溢出地计算两个值的中点; std::to_array 将 array-like 对象转换为 std::array
  • 新增一系列新库函数; jthread ;编译时源代码分析 source_location 库; u8string

C++23 新特性

  • 显式 this 参数: this Self& self ,搭配模板实现同时定义函数的 const 和非 const 版本,也可以实现递归 lambda 函数;
  • 多元 operator[] :支持逗号分隔的多维下标,可以搭配参数包转发;同时提供多维数组视图 mdspan ,可以用于包装一维数组模拟多维数组;
  • 标准模块 stdstd.compact
  • unexpected()and_thenor_else 等借鉴自 Rust 等语言的函数,错误码类型类似 optional;
  • 进一步扩展 Ranges:转换函数 to
  • print()println() :配合 format 输出;
  • 堆栈跟踪库 stacktrace
  • C++26 新特性(猜测):
    • 静态反射;
    • Executors:获取线程池对象,并分配任务;
    • Sender / Receiver:Sender 创建后不立即执行调度任务,传递给 Receiver 后在执行;支持使用通用异步算法实现链式调用;Scheduler 返回 Sender 工厂;
    • 标准网络库 Network;
    • 高性能计算(线性代数等);
    • Coroutines 扩展: std::lazy
    • 进一步扩展 Ranges;
    • Hive:Bucket Array 容器框架,用于高性能交易、游戏编程等场景的大量数据块操作;
    • 多线程无锁内存模型;
    • CPO 定制点对象:若引入的命名空间中仅有一个此函数名则无需写命名空间前缀;