C++20
概述
C++20 包含以下新的语言特性:
- 协程
- 概念
- 三路比较
- 指定初始化器
- [Lambda 表达式的模板语法](#Lambda 表达式的模板语法)
- [带初始化器的基于范围的 for 循环](#带初始化器的基于范围的 for 循环)
- [[[likely]] 和 [[unlikely]] 属性](#[[likely]] 和 [[unlikely]] 属性)
- [弃用 this 的隐式捕获](#弃用 this 的隐式捕获)
- 非类型模板参数中的类类型
- [constexpr 虚函数](#constexpr 虚函数)
- explicit(bool)
- 立即函数
- [using enum](#using enum)
- [参数包的 Lambda 捕获](#参数包的 Lambda 捕获)
- char8_t
- constinit
- VA_OPT
C++20 包含以下新的库特性:
- 文本格式化
- 概念库
- 同步缓冲输出流
- std::span
- 位运算
- 数学常量
- std::is_constant_evaluated
- [std::make_shared 支持数组](#std::make_shared 支持数组)
- [字符串的 starts_with 和 ends_with 方法](#字符串的 starts_with 和 ends_with 方法)
- 检查关联式容器是否包含元素
- std::bit_cast
- std::midpoint
- std::to_array
- std::bind_front
- 统一的容器擦除接口
- 三路比较辅助函数
- std::lexicographical_compare_three_way
- std::jthread
- 安全的整数比较
C++20 语言特性
协程
注意 :尽管这些示例从基础层面展示了如何使用协程,但代码编译时背后还有大量复杂的逻辑。这些示例并非旨在全面涵盖 C++20 协程的所有内容。由于标准库尚未提供
generator和task类,笔者使用了 cppcoro 库来编译这些示例。
协程 是一种特殊的函数,其执行过程可被挂起和恢复。要定义一个协程,函数体中必须包含 co_return、co_await 或 co_yield 关键字。C++20 的协程是无栈的;除非编译器将其优化掉,否则它们的状态会分配在堆上。
协程的一个示例是生成器(generator) 函数,它会在每次调用时生成(产出)一个值:
c++
generator<int> range(int start, int end) {
while (start < end) {
co_yield start;
start++;
}
// 函数末尾会隐式执行 co_return:
// co_return;
}
for (int n : range(0, 10)) {
std::cout << n << std::endl;
}
上述 range 生成器函数会从 start 开始生成值,直到 end(不包含)为止,每次迭代步骤都会产出 start 中存储的当前值。生成器会在 range 的每次调用(本例中是 for 循环的每次迭代)之间维持其状态。co_yield 接收给定的表达式,产出(返回)其值,并在此处挂起协程。恢复执行时,会从 co_yield 之后的代码继续。
协程的另一个示例是任务(task),它是一种异步计算,会在等待该任务时执行:
c++
task<void> echo(socket s) {
for (;;) {
auto data = co_await s.async_read();
co_await async_write(s, data);
}
// 函数末尾会隐式执行 co_return:
// co_return;
}
本例中引入了 co_await 关键字。该关键字接收一个表达式,如果等待的对象(此处为读或写操作)尚未就绪,则挂起执行;否则继续执行。(注意,底层实现中 co_yield 会使用 co_await。)
使用任务惰性求值一个值:
c++
task<int> calculate_meaning_of_life() {
co_return 42;
}
auto meaning_of_life = calculate_meaning_of_life();
// ...
co_await meaning_of_life; // 结果为 42
概念
概念(Concepts) 是具名的编译期谓词,用于约束类型。其语法形式如下:
template <模板参数列表>
concept 概念名 = 约束表达式;
其中 约束表达式 会求值为一个 constexpr 布尔值。约束 应描述类型的语义要求,例如某类型是否为数值类型、是否可哈希等。如果给定类型不满足其绑定的概念(即 约束表达式 返回 false),编译器会抛出错误。由于约束在编译期求值,它们能提供更易理解的错误信息,并保障运行时安全。
c++
// `T` 不受任何约束限制
template <typename T>
concept always_satisfied = true;
// 限制 `T` 为整数类型
template <typename T>
concept integral = std::is_integral_v<T>;
// 限制 `T` 同时满足 `integral` 约束且为有符号类型
template <typename T>
concept signed_integral = integral<T> && std::is_signed_v<T>;
// 限制 `T` 同时满足 `integral` 约束且不满足 `signed_integral` 约束
template <typename T>
concept unsigned_integral = integral<T> && !signed_integral<T>;
有多种语法形式可用于强制执行概念约束:
c++
// 函数参数的约束形式:
// `T` 是受约束的类型模板参数
template <my_concept T>
void f(T v);
// `T` 是受约束的类型模板参数
template <typename T>
requires my_concept<T>
void f(T v);
// `T` 是受约束的类型模板参数
template <typename T>
void f(T v) requires my_concept<T>;
// `v` 是受约束的推导参数
void f(my_concept auto v);
// `v` 是受约束的非类型模板参数
template <my_concept auto v>
void g();
// 自动推导变量的约束形式:
// `foo` 是受约束的自动推导值
my_concept auto foo = ...;
// Lambda 表达式的约束形式:
// `T` 是受约束的类型模板参数
auto f = []<my_concept T> (T v) {
// ...
};
// `T` 是受约束的类型模板参数
auto f = []<typename T> requires my_concept<T> (T v) {
// ...
};
// `T` 是受约束的类型模板参数
auto f = []<typename T> (T v) requires my_concept<T> {
// ...
};
// `v` 是受约束的推导参数
auto f = [](my_concept auto v) {
// ...
};
// `v` 是受约束的非类型模板参数
auto g = []<my_concept auto v> () {
// ...
};
requires 关键字可用于启动 requires 子句或 requires 表达式:
c++
template <typename T>
requires my_concept<T> // `requires` 子句
void f(T);
template <typename T>
concept callable = requires (T f) { f(); }; // `requires` 表达式
template <typename T>
requires requires (T x) { x + x; } // 同一行同时出现 `requires` 子句和表达式
T add(T a, T b) {
return a + b;
}
注意,requires 表达式中的参数列表是可选的。requires 表达式中的每个约束属于以下类型之一:
- 简单约束 - 断言给定表达式是合法的。
c++
template <typename T>
concept callable = requires (T f) { f(); };
- 类型约束 - 以
typename关键字后跟类型名表示,断言给定类型名是合法的。
c++
struct foo {
int foo;
};
struct bar {
using value = int;
value data;
};
struct baz {
using value = int;
value data;
};
// 使用 SFINAE,仅当 `T` 是 `baz` 时启用
template <typename T, typename = std::enable_if_t<std::is_same_v<T, baz>>>
struct S {};
template <typename T>
using Ref = T&;
template <typename T>
concept C = requires {
// 对类型 `T` 的约束:
typename T::value; // A) 拥有名为 `value` 的内部成员
typename S<T>; // B) 必须存在合法的类模板特化 `S<T>`
typename Ref<T>; // C) 必须是合法的别名模板替换
};
template <C T>
void g(T a);
g(foo{}); // 错误:不满足约束 A
g(bar{}); // 错误:不满足约束 B
g(baz{}); // 正确
- 复合约束 - 大括号中的表达式后跟后置返回类型或类型约束。
c++
template <typename T>
concept C = requires(T x) {
{*x} -> std::convertible_to<typename T::inner>; // 表达式 `*x` 的类型可转换为 `T::inner`
{x + 1} -> std::same_as<int>; // 表达式 `x + 1` 满足 `std::same_as<decltype((x + 1))>`
{x * 1} -> std::convertible_to<T>; // 表达式 `x * 1` 的类型可转换为 `T`
};
- 嵌套约束 - 以
requires关键字表示,指定额外的约束(例如对局部参数的约束)。
c++
template <typename T>
concept C = requires(T x) {
requires std::same_as<sizeof(x), size_t>;
};
另见:概念库。
三路比较
C++20 引入了太空船运算符(<=>),作为编写比较函数的新方式,可减少样板代码,并帮助开发者定义更清晰的比较语义。定义三路比较运算符后,编译器会自动生成其他比较运算符函数(即 ==、!=、< 等)。
C++20 引入了三种排序类型:
std::strong_ordering:强排序区分"相等"(完全相同且可互换)的元素。包含less(更小)、greater(更大)、equivalent(等价)和equal(相等)四种排序结果。适用场景示例:在列表中查找特定值、整数比较、区分大小写的字符串比较。std::weak_ordering:弱排序区分"等价"(不完全相同,但比较时可互换)的元素。包含less、greater和equivalent三种排序结果。适用场景示例:不区分大小写的字符串比较、排序操作、仅比较类的部分可见成员。std::partial_ordering:部分排序遵循弱排序的原则,但包含"无法比较"的情况。包含less、greater、equivalent和unordered(无序,无法比较)四种排序结果。适用场景示例:浮点数值比较(例如NaN)。
默认的三路比较运算符会按成员逐一比较:
c++
struct foo {
int a;
bool b;
char c;
// 先比较 `a`,再比较 `b`,最后比较 `c`......
friend auto operator<=>(const foo&) const = default;
};
foo f1{0, false, 'a'}, f2{0, true, 'b'};
f1 < f2; // 结果为 true
f1 == f2; // 结果为 false
f1 >= f2; // 结果为 false
也可自定义比较逻辑:
c++
struct foo {
int x;
bool b;
char c;
friend std::strong_ordering operator<=>(const foo& other) const {
return x <=> other.x;
}
};
foo f1{0, false, 'a'}, f2{0, true, 'b'};
f1 < f2; // 结果为 false
f1 == f2; // 结果为 true
f1 >= f2; // 结果为 true
指定初始化器
C 风格的指定初始化器语法。未在指定初始化器列表中显式列出的成员字段会被默认初始化。
c++
struct A {
int x;
int y;
int z = 123;
};
A a {.x = 1, .z = 2}; // a.x == 1,a.y == 0,a.z == 2
Lambda 表达式的模板语法
在 Lambda 表达式中使用熟悉的模板语法。
c++
auto f = []<typename T>(std::vector<T> v) {
// ...
};
带初始化器的基于范围的 for 循环
该特性简化了常见的代码模式,有助于缩小变量作用域,并为一个常见的生命周期问题提供了优雅的解决方案。
c++
for (auto v = std::vector{1, 2, 3}; auto& e : v) {
std::cout << e;
}
// 输出 "123"
[[likely]] 和 [[unlikely]] 属性
向优化器提供提示,表明标记的语句被执行的概率很高/很低。
c++
switch (n) {
case 1:
// ...
break;
[[likely]] case 2: // 认为 n == 2 的概率远高于 n 的其他取值
// ...
break;
}
如果 likely/unlikely 属性出现在 if 语句的右括号之后,表明该分支的子语句(函数体)被执行的概率很高/很低。
c++
int random = get_random_number_between_x_and_y(0, 3);
if (random > 0) [[likely]] {
// if 语句体
// ...
}
该属性也可应用于迭代语句的子语句(循环体):
c++
while (unlikely_truthy_condition) [[unlikely]] {
// while 语句体
// ...
}
弃用 this 的隐式捕获
在 Lambda 捕获中通过 [=] 隐式捕获 this 现已被弃用;建议通过 [=, this] 或 [=, *this] 显式捕获。
c++
struct int_value {
int n = 0;
auto getter_fn() {
// 不良写法:
// return [=]() { return n; };
// 良好写法:
return [=, *this]() { return n; };
}
};
非类型模板参数中的类类型
类类型现在可用于非类型模板参数。作为模板实参传递的对象类型为 const T(T 是对象的类型),且拥有静态存储期。
c++
struct foo {
foo() = default;
constexpr foo(int) {}
};
template <foo f = {}>
auto get_foo() {
return f;
}
get_foo(); // 使用隐式构造函数
get_foo<foo{123}>();
constexpr 虚函数
虚函数现在可以是 constexpr 的,并能在编译期求值。constexpr 虚函数可以覆盖非 constexpr 虚函数,反之亦然。
c++
struct X1 {
virtual int f() const = 0;
};
struct X2: public X1 {
constexpr virtual int f() const { return 2; }
};
struct X3: public X2 {
virtual int f() const { return 3; }
};
struct X4: public X3 {
constexpr virtual int f() const { return 4; }
};
constexpr X4 x4;
x4.f(); // 结果为 4
explicit(bool)
在编译期有条件地选择构造函数是否为显式(explicit)。explicit(true) 等价于直接指定 explicit。
c++
struct foo {
// 指定非整数类型(字符串、浮点数等)需要显式构造
template <typename T>
explicit(!std::is_integral_v<T>) foo(T) {}
};
foo a = 123; // 正确
foo b = "123"; // 错误:显式构造函数不参与候选(显式说明符求值为 true)
foo c {"123"}; // 正确
立即函数
与 constexpr 函数类似,但带有 consteval 说明符的函数必须生成常量。这类函数被称为立即函数(immediate functions)。
c++
consteval int sqr(int n) {
return n * n;
}
constexpr int r = sqr(100); // 正确
int x = 100;
int r2 = sqr(x); // 错误:'x' 的值无法用于常量表达式
// 若 `sqr` 是 `constexpr` 函数则正确
using enum
将枚举的成员引入当前作用域,提升代码可读性。
之前的写法:
c++
enum class rgba_color_channel { red, green, blue, alpha };
std::string_view to_string(rgba_color_channel channel) {
switch (channel) {
case rgba_color_channel::red: return "red";
case rgba_color_channel::green: return "green";
case rgba_color_channel::blue: return "blue";
case rgba_color_channel::alpha: return "alpha";
}
}
改进后的写法:
c++
enum class rgba_color_channel { red, green, blue, alpha };
std::string_view to_string(rgba_color_channel my_channel) {
switch (my_channel) {
using enum rgba_color_channel;
case red: return "red";
case green: return "green";
case blue: return "blue";
case alpha: return "alpha";
}
}
参数包的 Lambda 捕获
按值捕获参数包:
c++
template <typename... Args>
auto f(Args&&... args){
// 按值捕获:
return [...args = std::forward<Args>(args)] {
// ...
};
}
按引用捕获参数包:
c++
template <typename... Args>
auto f(Args&&... args){
// 按引用捕获:
return [&...args = std::forward<Args>(args)] {
// ...
};
}
char8_t
提供用于表示 UTF-8 字符串的标准类型。
c++
char8_t utf8_str[] = u8"\u0123";
constinit
constinit 说明符要求变量必须在编译期初始化。
c++
const char* g() { return "dynamic initialization"; }
constexpr const char* f() { return "constant initializer"; }
constinit const char* c = f(); // 正确
constinit const char* d = g(); // 错误:`g` 不是 constexpr,因此 `d` 无法在编译期求值
VA_OPT
辅助支持变长宏,若变长宏非空,则展开为给定的参数;若为空,则不展开任何内容。
c++
#define F(...) f(0 __VA_OPT__(,) __VA_ARGS__)
F(a, b, c) // 展开为 f(0, a, b, c)
F() // 展开为 f(0)
C++20 库特性
文本格式化
标准库新增了编译期检查的字符串格式化库,通过 std::format 实现。也可通过 std::vformat 及其他辅助工具在运行时对动态格式化字符串进行文本格式化。文本格式化遵循指定的规范。
std::format 的第一个参数是格式字符串,后续是可变数量的参数。若格式化失败,编译会直接报错:
cpp
std::format("{}", 123); // 正确 ------ 返回 "123"
std::format("{} {}", 123); // 错误 ------ 参数数量不足
std::format("{} {}", "Here's a number:", 123); // 正确
基于运行时创建的格式化器格式化字符串:
cpp
std::string fmt = "{} {}";
fmt += "{}{}";
std::vformat(fmt, std::make_format_args("Here's a number:", 1, 2, 3));
// 正确 ------ 返回 "Here's a number: 123"
若格式化失败(例如格式字符串无效),std::vformat 会抛出 std::format_error 异常。
格式化自定义类型:
c++
struct fraction {
int numerator;
int denominator;
};
template <>
struct std::formatter<fraction> {
constexpr auto parse(std::format_parse_context& ctx) {
return ctx.begin();
}
auto format(const fraction& f, std::format_context& ctx) const {
return std::format_to(ctx.out(), "{0:d}/{1:d}", f.numerator, f.denominator);
}
};
fraction f{1, 2};
std::format("{}", f); // 结果为 "1/2"
概念库
标准库也提供了一系列概念,用于构建更复杂的自定义概念。其中部分概念如下:
核心语言概念:
same_as- 指定两种类型完全相同。derived_from- 指定某类型派生自另一类型。convertible_to- 指定某类型可隐式转换为另一类型。common_with- 指定两种类型拥有共同类型。integral- 指定某类型为整数类型。default_constructible- 指定某类型的对象可默认构造。
比较概念:
boolean- 指定某类型可用于布尔上下文。equality_comparable- 指定operator==是等价关系。
对象概念:
movable- 指定某类型的对象可移动和交换。copyable- 指定某类型的对象可拷贝、移动和交换。semiregular- 指定某类型的对象可拷贝、移动、交换且可默认构造。regular- 指定某类型是正则类型 ,即同时满足semiregular和equality_comparable。
可调用概念:
invocable- 指定可调用类型可使用给定的参数类型集进行调用。predicate- 指定可调用类型是布尔谓词。
另见:概念。
同步缓冲输出流
为包装的输出流缓冲输出操作,确保同步(即输出不会交错)。
c++
std::osyncstream{std::cout} << "The value of x is:" << x << std::endl;
std::span
span 是容器的视图(即不拥有数据),提供对连续元素组的边界检查访问。由于视图不拥有元素,其构造和拷贝的开销极低------可以简单理解为视图持有指向数据的引用。相比于手动维护指针/迭代器和长度字段,span 将两者封装在单个对象中。
span 可分为动态大小(dynamic-sized)和固定大小(fixed-sized,称为其范围(extent) )两种。固定大小的 span 能受益于编译期边界检查。
span 不会传递常量性(const),因此要构造只读的 span,需使用 std::span<const T>。
示例:使用动态大小的 span 打印不同容器中的整数。
c++
void print_ints(std::span<const int> ints) {
for (const auto n : ints) {
std::cout << n << std::endl;
}
}
print_ints(std::vector{ 1, 2, 3 });
print_ints(std::array<int, 5>{ 1, 2, 3, 4, 5 });
int a[10] = { 0 };
print_ints(a);
// 其他容器同理
示例:固定大小的 span 若与容器的范围不匹配,编译会失败。
c++
void print_three_ints(std::span<const int, 3> ints) {
for (const auto n : ints) {
std::cout << n << std::endl;
}
}
print_three_ints(std::vector{ 1, 2, 3 }); // 错误
print_three_ints(std::array<int, 5>{ 1, 2, 3, 4, 5 }); // 错误
int a[10] = { 0 };
print_three_ints(a); // 错误
std::array<int, 3> b = { 1, 2, 3 };
print_three_ints(b); // 正确
// 若需要,可手动构造 span:
std::vector c{ 1, 2, 3 };
print_three_ints(std::span<const int, 3>{ c.data(), 3 }); // 正确:指定指针和长度字段
print_three_ints(std::span<const int, 3>{ c.cbegin(), c.cend() }); // 正确:使用迭代器对
位运算
C++20 新增 <bit> 头文件,提供若干位运算函数,包括 popcount(统计置位位数)。
c++
std::popcount(0u); // 0
std::popcount(1u); // 1
std::popcount(0b1111'0000u); // 4
数学常量
<numbers> 头文件中定义了数学常量,包括圆周率(PI)、自然常数(e)等。
c++
std::numbers::pi; // 3.14159...
std::numbers::e; // 2.71828...
std::is_constant_evaluated
一个谓词函数,在编译期上下文中调用时返回 true,运行时调用时返回 false。
c++
constexpr bool is_compile_time() {
return std::is_constant_evaluated();
}
constexpr bool a = is_compile_time(); // true
bool b = is_compile_time(); // false
std::make_shared 支持数组
c++
auto p = std::make_shared<int[]>(5); // 指向 `int[5]` 的指针
// 或
auto p = std::make_shared<int[5]>(); // 指向 `int[5]` 的指针
字符串的 starts_with 和 ends_with 方法
字符串(及字符串视图)新增 starts_with 和 ends_with 成员函数,用于检查字符串是否以指定字符串开头/结尾。
c++
std::string str = "foobar";
str.starts_with("foo"); // true
str.ends_with("baz"); // false
检查关联式容器是否包含元素
集合(set)、映射(map)等关联式容器新增 contains 成员函数,可替代"查找并检查迭代器是否到达末尾"的惯用写法。
c++
std::map<int, char> map {{1, 'a'}, {2, 'b'}};
map.contains(2); // true
map.contains(123); // false
std::set<int> set {1, 2, 3};
set.contains(2); // true
std::bit_cast
一种更安全的方式,将对象从一种类型重新解释为另一种类型。
c++
float f = 123.0;
int i = std::bit_cast<int>(f);
std::midpoint
安全计算两个整数的中点(避免溢出)。
c++
std::midpoint(1, 3); // 结果为 2
std::to_array
将给定的数组/"类数组"对象转换为 std::array。
c++
std::to_array("foo"); // 返回 `std::array<char, 4>`
std::to_array<int>({1, 2, 3}); // 返回 `std::array<int, 3>`
int a[] = {1, 2, 3};
std::to_array(a); // 返回 `std::array<int, 3>`
std::bind_front
将前 N 个参数(N 为 std::bind_front 中函数参数后的参数数量)绑定到指定的自由函数、Lambda 表达式或成员函数。
c++
const auto f = [](int a, int b, int c) { return a + b + c; };
const auto g = std::bind_front(f, 1, 1);
g(1); // 结果为 3
统一的容器擦除接口
为字符串、列表、向量、映射等多种 STL 容器提供 std::erase 和/或 std::erase_if 函数。
按值擦除元素使用 std::erase,按谓词条件擦除元素使用 std::erase_if。两个函数均返回被擦除的元素数量。
c++
std::vector v{0, 1, 0, 2, 0, 3};
std::erase(v, 0); // v 变为 {1, 2, 3}
std::erase_if(v, [](int n) { return n == 0; }); // v 仍为 {1, 2, 3}
三路比较辅助函数
为比较结果提供具名判断的辅助函数:
c++
std::is_eq(0 <=> 0); // 结果为 true
std::is_lteq(0 <=> 1); // 结果为 true
std::is_gt(0 <=> 1); // 结果为 false
另见:三路比较。
std::lexicographical_compare_three_way
使用三路比较按字典序比较两个范围,并生成适用的最强比较类别类型的结果。
c++
std::vector a{0, 0, 0}, b{0, 0, 0}, c{1, 1, 1};
auto cmp_ab = std::lexicographical_compare_three_way(
a.begin(), a.end(), b.begin(), b.end());
std::is_eq(cmp_ab); // 结果为 true
auto cmp_ac = std::lexicographical_compare_three_way(
a.begin(), a.end(), c.begin(), c.end());
std::is_lt(cmp_ac); // 结果为 true
std::jthread
一种执行线程(类似 std::thread),会在析构时自动合并(join),且可被发送停止信号。
与 std::thread 不同(需手动检查线程是否可合并,再调用 join),std::jthread 会通过其析构函数自动尝试合并。
此外,可通过调用 std::jthread::request_stop 或线程的 stop_source 请求 std::jthread 停止:
cpp
std::jthread t{
[](std::stop_token stoken) {
while (!stoken.stop_requested()) {
std::this_thread::sleep_for(1s);
}
}
};
// 从线程对象发起停止请求:
t.request_stop();
// 或通过 stop_source 发起:
std::stop_source stopSource = t.get_stop_source();
stopSource.request_stop();
std::stop_token 可用于查询线程的停止状态。
安全的整数比较
比较整数(包括不同类型的整数),避免整数转换带来的风险。
cpp
-1 > 0U; // 结果为 true(非安全比较,存在符号扩展问题)
std::cmp_greater(-1, 0U); // 结果为 false(安全比较)
std::cmp_equal(0U, 0); // 结果为 true
std::cmp_less_equal(-1, 1U); // 结果为 true
std::in_range<unsigned>(-1); // 结果为 false
std::in_range<char>(999999); // 结果为 false