C++14, 17, 20 的新政策
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