1. C++14
不同于重量级的 C++11 给 C++ 世界带来的脱胎换骨焕然一新,C++14 的体量就比较小。
1.1. 语法级
1.1.1. 字面量
是的,二进制字面量终于来了,如 101010b。数字分位符也来了,如 int i = 424'242;。
1.1.2. lambda 形参类型推导
lambda 的形参类型可使用 auto 推导,如:
auto l = [](auto i) { return i + 1; };
1.1.3. 函数返回类型推导
函数的返回类型可使用 auto 推导,如:
auto f(int i) {
return i + 1;
}
如果函数中有多个 return 语句,则必须可推断为相同的类型。如果函数中存在递归调用,则递归调用之前必须有至少一个可推断返回类型的 return 语句。
1.1.4. constexpr 函数
对 constexpr 函数的限制有所放宽,constexpr 函数中可包含:
- 任何声明,除了 static 变量、thread_local 变量、没有初始化的变量
- 分支和循环语句
- 表达式可改变一个对象的值,只需该对象的生命周期在函数内
1.1.5. 属性
使用 deprecated 属性会在编译期输出警告,如:
[[deprecated("f is thread-unsafe. Use g instead.")]]
void f();
1.2. 标准库级
1.2.1. 自定义字面量
1.2.1.1. 字符串
头文件
<string>
命名空间std::literals::string_literals
s,创建 std::basic_string,如:
auto str = "abc"s; // std::string s;
1.2.1.2. 时间
头文件
<chrono>
命名空间std::literals::chrono_literals
h、min、s、ms、us、ns,创建 std::chrono::duration,如:
auto dur = 42s; // std::chrono::seconds dur;
1.2.1.3. 复数
头文件
<complex>
命名空间std::literals::complex_literals
if、i、il,创建 std::complex<float>、std::complex<double>、std::complex<long double>,如:
auto z = 42i; // std::complex<double> z;
1.2.2. 容器
1.2.2.1. 元组
头文件
<utility>
std::get 函数,当元组中只有一个字段属于某种类型,则可使用该类型来访问该字段,如:
std::tuple<int, std::string, std::string> t(42, "abc", "abc");
int i = std::get<int>(t);
1.2.3. 编译时元编程
头文件
<type_traits>
std::is_final 类用于断言一个类是否禁止继承。
1.2.4. 多线程
头文件
<shared_mutex>
1.2.4.1. shared_timed_mutex
std::shared_timed_mutex 类作为读写互斥量,lock_shared、try_lock_shared、try_lock_shared_for、try_lock_shared_until 和 unlock_shared 方法用于读互斥,而在 std::timed_mutex 中也包含同名的 lock、try_lock、try_lock_for、try_lock_until 和 unlock 方法用于写互斥。
1.2.4.2. shared_lock
std::shared_lock 类与 std::unique_lock 的方法构成完全相同,只是 std::shared_lock 必须基于一个可共享的互斥量,如 std::shared_timed_mutex。
2. C++17
2.1. 语法级
2.1.1. 结构化绑定
结构化绑定声明可用于数组、std:tuple、std::pair,或用户定义的结构,如:
int arr[2] = {42, 42};
auto [i, j] = arr;
auto &[ri, rj] = arr;
2.1.2. 折叠表达式
折叠表达式简化了模板的变长类型参数的使用,有以下四种折叠:
-
(e op ...),展开为(e1 op (e2 op (... op eN))) -
(... op e),展开为(((e1 op e2) op ...) op eN) -
(e op ... op i),展开为(e1 op (e2 op (... op (eN op i)))) -
(i op ... op e),展开为((((i op e1) op e2) op ...) op eN)
其中,e为包含模板变长类型参数对应的形参包的表达式,i为不包含变长形参包的表达式,op为一个二元操作符,e1、e2至eN为变长形参包的每个分量对应的表达式,如:
template<typename... Args>
void print(Args... args) {
(std::cout << ... << args) << '\n';
// print(42, 'a', "abc"); =>
// (((std::cout << 42) << 'a') << "abc") << '\n';
}
template<typename T, typename... Args>
void push_back(std::vector<T> &v, Args... args) {
(v.push_back(args), ...);
// push_back(v, 42, 42.0, 'a'); =>
// ((v.push_back(42), v.push_back(42.0)), v.push_back('a'));
}
2.1.3. lamdba 捕获 *this
lambda 以拷贝构造捕获 this,如:
class Cls {
void f() {
[*this](){}();
[=, *this](){}();
}
};
2.1.4. constexpr if
constexpr if 属于元编程的范畴,使用一个常量表达式作为条件,在编译时选择分支,未被选择的分支最终不会被编译,当然在运行时也不会有跳转,对运行时的性能有所增强,如:
template<typename T> void f() {
if constexpr (sizeof(T) == 8) {
} else if constexpr (sizeof(T) == 4) {
} else {
}
}
2.1.5. if/switch 初始化
Golang 中惯常使用的,让外层命名空间更精简的语句,如:
if (int i = f(); g(i) > i) {
2.1.6. 类模板类型参数推导
初始化一个类模板的对象,模板的类型参数可自动推导,如:
std::tuple t(42, 42.0); // std::tuple<int, double>
2.1.7. 模板非类型参数类型推导
模板非类型参数的类型可使用 auto 推导,如:
template<auto i> class Cls;
Cls<42> c; // template<int i> class Cls
2.1.8. 嵌套命名空间简化
对于嵌套的命名空间,之前需要写成如:
namespace N {
namespace N1 {
}
}
现可简化为:
namespace N::N1 {
}
2.2. 标准库级
2.2.1. 容器
2.2.1.1. 字符串
头文件
<charconv>
std::from_chars 和 std::to_chars 函数用于字符串转换。
2.2.1.2. 字符串 view
头文件 `<string_view>
参考文档
std::basic_string_view 类类似 std::basic_string 的读写行为,但不掌管底层内存的生命周期。
2.2.1.3. map
头文件
<map>
std::map 的 try_emplace 方法,仅当 key 不存在时才执行与 emplace 相同的操作。
std::map 的 insert_or_assign 方法,顾名思义。
std::map 的 extract 方法,更换 map 中分量的 key 而不重新分配空间的唯一方式,如:
std::map<int, std::string> m;
auto node = m.extract(42);
node.key() = 7;
m.insert(std::move(node));
std::map 的 merge 方法,将传入容器中的分量抽取到 this 中,key 存在的分量则不抽取,如:
std::map<int, std::string> m1 = {{1, "a"}, {2, "b"}};
std::map<int, std::string> m2 = {{2, "d"}, {3, "c"}};
m1.merge(m2);
// m1 == {{1, "a"}, {2, "b"}, {3, "c"}}
// m2 == {{2, "d"}}
2.2.1.4. 元组
头文件
<tuple>
std::apply 函数,将一个 std::tuple、std::pair 或 std::array 中的分量作为参数来调用可调用对象,如:
std::apply([](auto a, auto b) { return a + b; }, std::tuple(42, 42));
2.2.1.5. optional
头文件
<optional>
参考文档
std::optional 类,类似诸多含有 null-safety 特性的语言中的 Option 类,如:
std::optional<std::string> opt("abc");
std::string &rs = opt.value();
std::optional<int> opt1(std::nullopt);
opt1.has_value(); // false
2.2.1.6. any
头文件
<any>
参考文档
std::any 类,一种类型安全的泛型单值容器。注意这不是一个类模板,不同实际类型的对象之间可以相互赋值、交换,可放入同一个线性、关联容器,如:
std::any a(42);
int &ri = std::any_cast<int&>(a);
2.2.1.7. variant
头文件
<variant>
参考文档
std::variant 类,一种类型安全的联合体,不可存放引用、数组和 void 类型,如:
std::variant<std::string, int> var(42);
int &ri = std::get<int>(var); // 42
int &rj = std::get<1>(var); // 42
var.index(); // 1
2.2.2. 算法
头文件
<algorithm>
std::clamp 函数用于夹。
头文件
<numeric>
std::reduce 函数,是的就是那个 reduce。
std::inclusive_scan 和 std::exclusive_scan 函数用于前缀运算。参考文档
std::gcd 函数用于求最大公约数。std::lcm 函数用于求最小公倍数。
2.2.3. 文件系统
头文件
<filesystem>
参考文档
文件系统 API 终于正式进入标准库。
2.2.4. 动态内存管理
2.2.4.1. new 操作符
new 和 new[] 操作符现在可以传入第二个类型为 std::align_val_t 的参数用于内存对齐。
2.2.4.2. 分配器
头文件
<memory_resource>
std::pmr::polymorphic_allocator 类实现了一个可供容器使用的运行时多态分配器,由一个 std::pmr::memory_resource 类的派生类提供分配策略,包括简单使用 new 和 delete 操作符的 std::pmr::new_delete_resource 类和池化的 std::pmr::synchronized_pool_resource 类等。
2.2.4.3. 未初始化内存算法
头文件
<memory>
std::uninitialized_move 函数将对象移动构造至未初始化内存。std::uninitialized_default_construct 函数缺省构造至未初始化内存。std::uninitialized_default_construct 函数值初始化构造至未初始化内存。
std::destroy_at 函数析构指针指向的对象。std::destroy 函数析构迭代器指向的对象。
2.2.5. 编译时元编程
头文件
<type_traits>
2.2.5.1. 静态断言
std::is_swappable_with 类用于断言两个类型之间是否可调用 std::swap。
std::is_invocable 类用于断言一个可调用类型是否可由一个类型序列作为参数来调用。
std::is_aggregate 类用于断言一个类型是否聚合类型。
2.2.5.2. 模板元类
std::conjunction 类构成类型之间的逻辑与。参考文档
std::disjunction 类构成类型之间的逻辑或。参考文档
std::negation 类构成类型的逻辑非。参考文档
2.2.6. 多线程
2.2.6.1. shared_mutex
头文件
<shared_mutex>
参考文档
shared_mutex 类之于 shared_timed_mutex,如同 mutex 之于 timed_mutex。shared_mutex 之于 mutex,如同 shared_timed_mutex 之于 timed_mutex。
2.2.6.2. scoped_lock
头文件
<mutex>
参考文档
scoped_lock 类用于 RAII 式的互斥量包装,在析构时释放互斥量。
2.2.7. 数学特殊函数
头文件
<cmath>
参考文档
3. C++20
3.1. 语法级
3.1.1. 字符
char8_t 类型为 8 位字符类型,表示一个 UTF-8 的编码,与 unsigned char 性质相同。C++20 起使用前缀 u8 产生的字符串字面量,其字符类型变为 char8_t。
3.1.2. 三路比较操作符
是的你没有看错,C++20 居然增加了一个用非字母书写的二元操作符,一个看起来挺鬼畜的符号。三路比较操作符返回一个序类型,参考 3.2.1. 章节。
3.1.3. 初始化
指派初始化器,每个指派符必须指定一个直接非静态数据成员,初始化表达式中指定的顺序必须与类型定义中的成员顺序相同,未指定的字段进行值初始化,如:
struct S { int a; int b; int c; };
S s{ .a = 1, .c = 2 };
3.1.4. 范围 for 初始化
类似 if 初始化,如:
for (auto v = f(); auto e : v) {
3.1.5. 常量表达式
consteval 关键字声明函数立即函数,即每次调用必须产生常量表达式,蕴含 constexpr 和 inline。
constinit 关键字声明变量拥有静态或线程生命周期。
3.1.6. 概念
我们知道 C++ 的一项原则,要增加任何新内容,能在标准库实现的就不增加新语法。从 C++98 以来,即使是增加新语法,也无非是在现有语言要素上作出调整和补充,比如类型的自动推导、模板的变长类型参数就算其中的重大更新了,而闭包也只是仿函类的语法糖。但这次的概念(concept),则是新增了一项全新的语言要素类型,堪称 C++20 三巨头之首。
概念是一个对模板类型实参的约束,可对模板类型实参进行编译时元编程的断言。而一个模板的模板类型形参可声明受概念的约束,编译器实例化模板时,会对模板类型实参执行概念的编译时元编程代码,检查其是否满足约束。类似的,我们能在诸如 Rust/Java 中约束泛型参数必须继承于某个类或实现了某个接口,相比之下显然 C++ 概念的表达能力更强,直逼 Haskell 的 typeclass。
概念的定义,如:
template<typename T, typename U>
concept ConceptA = std::is_xxx<T, U>::value && std::is_yyy<U, T>::value;
template<typename T, typename U>
concept ConceptB = std::is_zzz<T, U>::value || !ConceptA<U, T>;
template<typename T, typename U>
concept ConceptC = requires(T t, U u) {
t + u;
typename T::U;
};
显然,可以将概念看作一个常量布尔表达式或一个函数。概念不能递归定义,不能显式实例化、特化,不能约束另一个概念。以 requires 开头的表达式类似一个函数,其 requires 体中的语句分为以下类型,可混合出现:
- 简单 requires:仅一个表达式。不进行求值,只检查语法合法,如:
template<typename T, typename U>
concept Concept = requires(T t, U u) {
t + u;
};
- 类型 requires:
typename跟一个类型名。检查类型名合法,如:
template<typename T>
concept Concept = requires(T t) {
typename T::U;
};
- 复合 requires:一个花括号包含的表达式,跟一个箭头,再跟一个编译时元编程断言类。检查花括号中的表达式对应的
decltype(())满足断言,如:
template<typename T>
concept Concept = requires(T t) {
{t + 1} -> std::same_as<T>; // to check std::same_as<decltype((t + 1)), T>::value
};
- 嵌套 requires:
requires跟一个约束。引用其他约束,如:
template<typename T>
concept Concept = requires(T t) {
requires ConceptA<T*>;
};
模板指定概念时,可在模板参数列表中用概念替代 typename,或在模板参数列表之后使用 requires 子句,或者声明的主体之后使用 requires 子句,或在以上三处中的多处同时出现而构成逻辑与的关系,如:
template<ConceptA T, ConceptB<int> U> requires ConceptC<T, U>
void f(T, U) requires ConceptD<T>;
// T is constrainted by ConceptA<T> and ConceptC<T, U> and ConceptD<T>
// U is constrainted by ConceptB<U, int> and ConceptC<T, U>
requires 子句中也可使用逻辑与、或、非,如:
template<typename T> requires (ConceptA<T> && ConceptB<T>)
void f(T);
例如,一个函数接受一个对象及其 run 方法的参数,run 方法有特定的参数列表,如果对象存在此 run 方法,则传参调用该方法并返回 true,否则不调用并返回 false。显然,在动态语言中这个函数很容易实现,但在 C++ 这样的静态语言中则需要使用到编译时的模板元编程技巧。在 C++11 中,我们可以用两层的 sfinae 来实现:
#include <type_traits>
template<typename T, typename ...Args> struct has_method_run {
private:
template<typename U> static auto f(int) ->
decltype(std::declval<U>().run(std::declval<Args>()...), std::true_type());
template<typename U> static std::false_type f(char);
public:
enum { value = decltype(f<T>(0))::value };
};
template<typename T, typename ...Args> auto run(T &&t, Args &&...args) ->
typename std::enable_if<has_method_run<T, Args...>::value, bool>::type {
t.run(std::forward<Args>(args)...);
return true;
}
template<typename T, typename ...Args> auto run(T &&t, Args &&...args) ->
typename std::enable_if<!has_method_run<T, Args...>::value, bool>::type {
return false;
}
看起来有些天书。在 C++17 中,可以更优雅一点,我们可以使用 if constexpr 来消除 run 函数必需的重载,从而让 sfinae 减少到只有一层:
#include <type_traits>
template<typename T, typename ...Args> struct has_method_run {
private:
template<typename U> static auto f(int) ->
decltype(std::declval<U>().run(std::declval<Args>()...), std::true_type());
template<typename U> static std::false_type f(char);
public:
enum { value = decltype(f<T>(0))::value };
};
template<typename T, typename ...Args> bool run(T &&t, Args &&...args) {
if constexpr (has_method_run<T, Args...>::value) {
t.run(std::forward<Args>(args)...);
return true;
}
return false;
}
而在 C++20 中,我们可以使用 concept 让 sfinae 变得更优雅:
#include <type_traits>
template<typename T, typename ...Args> concept must_have_method_run = requires(T t, Args ...args) {
t.run(args...);
};
template<typename T, typename ...Args> struct has_method_run {
private:
template<typename U> requires must_have_method_run<U, Args...> static std::true_type f(int);
template<typename U> static std::false_type f(char);
public:
enum { value = decltype(f<T>(0))::value };
};
template<typename T, typename ...Args> bool run(T &&t, Args &&...args) {
if constexpr (has_method_run<T, Args...>::value) {
t.run(std::forward<Args>(args)...);
return true;
}
return false;
}
3.1.7. 协程
[参考文档](https://en.cppreference.com/w/cpp/language/coroutines)
C++20 三巨头之二。专门一篇
3.1.8. 模块
C++20 三巨头之三。模块与命名空间之间还是正交的,日你先人,略。
3.2. 标准库级
3.2.1. 比较与排序
头文件
<compare>
是的,在 Rust 当中会见到的那些抽象代数的概念,现在来到了 C++ 的标准库中。
std::partial_ordering 类实现了偏序概念。其中包含以下同类 constexpr 静态成员常量:less、equivalent、greater、unordered,分别表示小于、等价、大于、不可比较。不可隐式转换为 std::weak_ordering 和 std::strong_ordering。对于三路比较返回 std::partial_ordering 的类型,其 <、==、> 操作符可均返回 false。
std::weak_ordering 类实现了弱序概念。其中包含以下同类 constexpr 静态成员常量:less、equivalent、greater,分别表示小于、等价、大于。可隐式转换为 std::partial_ordering,不可隐式转换为 std::strong_ordering。对于三路比较返回 std::weak_ordering 的类型,其 <、==、> 操作符需有且仅有一个返回 true。
std::strong_ordering 类实现了强序概念。其中包含以下同类 constexpr 静态成员常量:less、equivalent、equal、greater,分别表示小于、等价、等价、大于。可隐式转换为 std::partial_ordering 和 std::weak_ordering。对于三路比较返回 std::strong_ordering 的类型,其 <、==、> 操作符需有且仅有一个返回 true。
例如,两个 int 类型的变量之间的三路比较操作符返回一个 std::strong_ordering 类型,而两个 double 类型则返回 std::partial_ordering 类型,因为浮点数存在不可比较的 NaN 值。以上三个序类均重载了 ==、<、>、<=、>=、<=> 操作符,可与同类型对象或 0 字面量进行比较。
std::common_comparison_category 类断言多个类之间能转换为的最强序类。
std::compare_three_way_result 类断言一个或两个类之间的三路比较操作符的返回类型。
3.2.2. 概念
头文件
<concepts>
参考文档
3.2.3. 工具库
3.2.3.1. 源码信息
头文件
<source_location>
参考文档
std::source_location 类用于表示源码信息,作为 __FILE__ 等宏的替代方案,如:
auto sl = std::source_location::current();
sl.line();
3.2.3.2. 格式化
头文件
<format>
参考文档
格式化库作为 printf 函数族的替代方案,如:
std::string s = std::format("{} {}", "Hello", "world");
3.2.3.3. 时间
头文件
<chrono>
参考文档
时间库中增加了日历和时区。
3.2.3.4. 函数绑定
头文件
<functional>
std::bind_front 函数将多个参数绑定到可调用对象的首部形参。
3.2.4. 容器
3.2.4.1. 字符串
头文件
<string>
std::string 终于有 starts_with 和 ends_with 方法了。
头文件
<cuchar>
std::c8rtomb 和 std::mbrtoc8 函数用于单个编点的 UTF-8 与窄多字节字符表示之间的转换。
3.2.4.2. span
头文件
<span>
参考文档
std::span 类模板用于抽象描述一个线性序列,可拥有静态或动态的长度。
3.2.4.3. range
头文件
<ranges>
参考文档
众所周知三巨头一定有四位,range 库正是 C++20 三巨头之四。
3.2.5. 算法
头文件
<numeric>
std::midpoint 函数用于计算中点。
头文件
<bit>
参考文档
提供二进制算法,如 std::popcount 函数作为 __builtin_popcount 的替代方案,std::countl_zero 函数作为 __builtin_clz 的替代方案。
3.2.6. 动态内存管理
头文件
<memory>
std::make_obj_using_allocator 函数用于创建对象!
std::make_shared 函数添加了大量重载。参考文档
3.2.7. 编译时元编程
头文件
<type_traits>
std::remove_cvref 类用于生成去掉引用类型和 cv 限定符的类型。
std::is_bounded_array 类用于断言一个类型是否是有已知固定长度的数组。
3.2.8. 多线程
3.2.8.1. osyncstream
头文件
<syncstream>
参考文档
std::osyncstream 作为线程安全的 std::ostream。
3.2.8.2. latch
头文件
<latch>
参考文档
std::latch 类作为单次线程屏障。其计数不能增加或重置,因此只能使用一次。count_down 方法减计数,arrive_and_wait 方法减计数且阻塞线程直至计数为零,wait 方法阻塞线程直至计数为零。
3.2.8.3. barrier
头文件
<barrier>
参考文档
std::barrier 类作为可复用线程屏障。不同于 latch,其计数为零时重置,因此可使用多次;不同于 latch,每个计数周期内,一个线程只能减计数一次。可指定一个初始计数和周期回调,计数为零时调用周期回调,周期回调返回之后,所有阻塞线程的方法才返回,并恢复初始计数。arrive 方法减计数,arrive_and_drop 方法减计数且减初始计数,arrive_and_wait 方法减计数且阻塞线程直至计数为零,wait 方法阻塞线程直至计数为零。
3.2.8.4. semaphore
头文件
<semaphore>
参考文档
std::counting_semaphore 类作为信号量。acquire 方法在计数大于 0 时减计数,否则阻塞线程直至成功减计数,release 方法加计数,同时包含 try_acquire 方法族。
3.2.9. 数学常数
头文件
<numbers>
参考文档
A. 附录
A.1. 关键字
A.1.1. C++11 中新增的关键字
alignas alignof char16_t char32_t constexpr decltype final noexcept nullptr override static_assert thread_local
A.1.2. C++11 中含义有变的关键字
auto class default delete export extern inline mutable sizeof struct using
A.1.3. C++17 中含义有变的关键字
register
A.1.4. C++20 中新增的关键字
char8_t concept consteval constinit co_await co_return co_yield module requires
A.1.5. C++20 中含义有变的关键字
export