C++ 20 新特性

语言特性

三路比较运算符

三路比较运算符表达式的形式为:左操作数 <=> 右操作数,表达式返回一个对象,使得

  • 若左操作数 < 右操作数则(a <=> b) < 0
  • 若左操作数 > 右操作数则(a <=> b) > 0
  • 而若左操作数和右操作数相等 / 等价则(a <=> b) == 0
1
2
3
4
5
6
7
8
9
10
11
12
13
int main() {
double foo = -0.0;
double bar = 0.0;

auto res = foo <=> bar;

if (res < 0)
std::cout << "-0 小于 0";
else if (res > 0)
std::cout << "-0 大于 0";
else // (res == 0)
std::cout << "-0 与 0 相等 ";
}

范围 for 中的初始化语句和初始化器

继 C++ 17 中在 ifswitch语句中添加初始化器后,C++ 20 在范围 for 中也实现了这个功能

1
2
for (auto n = v.size(); auto i : v) // 初始化语句(C++20)
std::cout << --n + i << ' ';

consteval

consteval指定函数是立即函数(immediate function),即每次调用该函数必须产生编译时常量。如果不能在编译期间执行,则编译失败

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
consteval int sqr(int n) {
return n * n;
}
constexpr int r = sqr(100); // OK

int x = 100;
int r2 = sqr(x); // 错误:调用不产生常量

consteval int sqrsqr(int n) {
return sqr(sqr(n)); // 在此点非常量表达式,但是 OK
}

constexpr int dblsqr(int n) {
return 2 * sqr(n); // 错误:外围函数并非 consteval 且 sqr(n) 不是常量
}

constint

constinit断言变量拥有静态初始化,即零初始化与常量初始化,否则程序非良构

1
2
3
4
5
const char *g() { return "dynamic initialization"; }
constexpr const char *f(bool p) { return p ? "constant initializer" : g(); }

constinit const char *c = f(true); // OK
// constinit const char *d = f(false); // 错误

概念(concepts)

概念(concepts)就是一种编译时谓词,指出一个或多个类型应如何使用,其能用于进行模板实参的编译时校验,以及基于类型属性的函数派发。

例如在老版本的 C++,如果想要定义一个只针对某个类型的函数模板,就只能通过类型萃取机制如 enable_if_t 写一些又臭又长的代码。例如想声明一个只针对整数的函数模板

1
2
3
4
5
template <typename T>
auto mod(std::enable_if_t<std::is_integral_v<T>, T> d)
{
return d % 10;
}

如果约束条件简单还行,但是如果条件复杂,则代码就会又臭又长,且难以进行复用。而在 C++ 20 中引入了 concepts,此时我们就可以用 concepts 来指定函数类型,例如:

1
2
3
4
5
6
7
8
template <class T>
concept integral = std::is_integral_v<T>;

template <integral T>
auto mod(T d)
{
return d % 10;
}

约束

约束是逻辑操作和操作数的序列,它了指定对模板实参的要求。它们可以在 requires 表达式中出现,也可以直接作为概念的主体。例如这里使用 requires 约束表达式写一个针对 utf-8 的 string 的约束类型u8string_t

1
2
3
4
5
template <typename T>
concept u8string_t = requires (T t)
{
t += u8"";
};

接着以这个约束类型声明一个模板函数 print,此时只能能够满足u8string_t 约束的类型才能够匹配当前模板

1
2
3
4
5
template <u8string_t T>
auto print(T t)
{
cout << t << endl;
}

此时以不同类型的 string 来尝试调用,此时只有 u8string 调用成功

1
2
3
4
5
6
7
8
9
10
11
12
int main()
{
string str;
u8string str_u8;
u16string str_u16;
u32string str_u32;

print(str); // 调用失败
print(str_u8); // 调用成功
print(str_u16); // 调用失败
print(str_u32); // 调用失败
}

协程

协程是能暂停执行以在之后恢复的函数。协程是无栈的:它们通过返回到调用方暂停执行,并且从栈分离存储恢复执行需要的数据。这样就可以编写异步执行的顺序代码(例如不使用显式的回调来处理非阻塞 I/O),还支持对惰性计算的无限序列上的算法及其他用途

如果函数的定义进行了下列操作之一,那么它是协程:

  • co_await 暂停执行,直到恢复
1
2
3
4
5
6
7
task<> tcp_echo_server() {
char data[1024];
while (true) {
std::size_t n = co_await socket.async_read_some(buffer(data));
co_await async_write(socket, buffer(data, n));
}
}
  • co_yield 暂停执行并返回一个值(协程无法return
1
2
3
4
generator<int> iota(int n = 0) {
while(true)
co_yield n++;
}
  • co_return完成执行并返回一个值
1
2
3
lazy<int> f() {
co_return 7;
}

注意:协程不能使用变长实参,普通的 return 语句,或占位符返回类型(autoConcept)。constexpr函数、构造函数、析构函数及 main 函数不能是协程

模块

C++ 20 中正式引入了模块的概念,模块是一个用于在翻译单元间分享声明和定义的语言特性。它们可以在某些地方替代使用头文件。其主要优点如下:

  • 没有头文件
  • 声明实现仍然可分离,但非必要
  • 可以显式指定导出哪些类或函数
  • 不需要头文件重复引入宏(include guards
  • 模块之间名称可以相同,并且不会冲突
  • 模块只处理一次,编译更快(头文件每次引入都需要处理,需要通过 pragma once 约束)
  • 预处理宏只在模块内有效
  • 模块的引入与引入顺序无关

创建模块

1
2
3
4
5
6
7
// helloworld.cpp
export module helloworld; // 模块声明
import <iostream>; // 导入声明

export void hello() { // 导出声明
std::cout << "Hello world!\n";
}

导入模块

1
2
3
4
5
6
// main.cpp
import helloworld; // 导入声明

int main() {
hello();
}

库特性

format

文本格式化库提供 printf 函数族的安全且可扩展的替用品。有意使之补充既存的 C++ I/O 流库并复用其基础设施,例如对用户定义类型重载的流插入运算符

1
2
3
4
5
6
7
std::string message = std::format("The answer is {}.", 42);
osyncstream
template<
class CharT,
class Traits = std::char_traits<CharT>,
class Allocator = std::allocator<CharT>
> class basic_osyncstream: public std::basic_ostream<CharT, Traits>

类模板 std::basic_osyncstreamstd::basic_syncbuf的便利包装。它提供机制以同步写入同一流的线程(主要用于解决 std::cout 线程不安全问题)

用法如下:

1
2
3
4
5
6
7
{
std::osyncstream sync_out(std::cout); // std::cout 的同步包装
sync_out << "Hello, ";
sync_out << "World!";
sync_out << std::endl; // 注意有冲入,但仍未进行
sync_out << "and more!\n";
} // 转移字符并冲入 std::cout

span

span是对象的连续序列上的无所有权视图。其所描述的对象能指代对象的相接序列,序列的首元素在零位置。span能拥有静态长度,该情况下序列中的元素数已知并编码于类型中,或拥有动态长度

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <algorithm>
#include <cstddef>
#include <iostream>
#include <span>

template<class T, std::size_t N> [[nodiscard]]
constexpr auto slide(std::span<T,N> s, std::size_t offset, std::size_t width) {
return s.subspan(offset, offset + width <= s.size() ? width : 0U);
}

template<class T, std::size_t N, std::size_t M> [[nodiscard]]
constexpr bool starts_with(std::span<T,N> data, std::span<T,M> prefix) {
return data.size() >= prefix.size()
&& std::equal(prefix.begin(), prefix.end(), data.begin());
}

template<class T, std::size_t N, std::size_t M> [[nodiscard]]
constexpr bool ends_with(std::span<T,N> data, std::span<T,M> suffix) {
return data.size() >= suffix.size()
&& std::equal(data.end() - suffix.size(), data.end(),
suffix.end() - suffix.size());
}

template<class T, std::size_t N, std::size_t M> [[nodiscard]]
constexpr bool contains(std::span<T,N> span, std::span<T,M> sub) {
return std::search(span.begin(), span.end(), sub.begin(), sub.end()) != span.end();
// return std::ranges::search(span, sub).begin() != span.end();
}

void print(const auto& seq) {
for (const auto& elem : seq) std::cout << elem << ' ';
std::cout << '\n';
}

int main()
{
constexpr int a[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 };
constexpr int b[] { 8, 7, 6 };

for (std::size_t offset{}; ; ++offset) {
constexpr std::size_t width{6};
auto s = slide(std::span{a}, offset, width);
if (s.empty())
break;
print(s);
}

static_assert(starts_with(std::span{a}, std::span{a,4})
&& starts_with(std::span{a+1, 4}, std::span{a+1,3})
&& !starts_with(std::span{a}, std::span{b})
&& !starts_with(std::span{a,8}, std::span{a+1,3})
&& ends_with(std::span{a}, std::span{a+6,3})
&& !ends_with(std::span{a}, std::span{a+6,2})
&& contains(std::span{a}, std::span{a+1,4})
&& !contains(std::span{a,8}, std::span{a,9}));
}

endian

endian主要用于判断当前机器是大端还是小端(之前只能通过整型截断或者 union 判断,较为麻烦)

  • 若所有标量类型均为小端,则 std::endian::native 等于std::endian::little
  • 若所有标量类型均为大端,则 std::endian::native 等于std::endian::big
  • 若所有标量类型拥有等于 1 的 sizeof,则端序无影响,且std::endian::littlestd::endian::bigstd::endian::native三个值相同
  • 若平台使用混合端序,则 std::endian::native 既不等于 std::endian::big 亦不等于std::endian::little
1
2
3
4
5
6
7
8
9
10
11
#include <bit>
#include <iostream>

int main() {

if constexpr (std::endian::native == std::endian::big)
std::cout << "big-endian\n";
else if constexpr (std::endian::native == std::endian::little)
std::cout << "little-endian\n";
else std::cout << "mixed-endian\n";
}

jthread

jthread即是通过 RAII 机制封装的 thread,其会在析构时自动调用join 防止线程 crash。同时其也是可中断的,可以搭配这些中断线程执行的相关类使用:

  • stop_token:查询线程是否中断
  • stop_source:请求线程停止运行
  • stop_callbackstop_token执行时,可以触发的回调函数

semaphore

信号量是一个轻量级的同步原语,可用来实现任何其他同步概念如 mutexshared_mutexlatchesbarriers

根据 LeastMaxValue 不同,主要分为两种:

  • counting_semaphore(多元信号量):counting_semaphore允许同一资源有多于一个同时访问,至少允许 LeastMaxValue 个同时的访问者
  • binary_semaphore(二元信号量):是 counting_semaphore 的特化的别名,其 LeastMaxValue 为 1 。实现可能将 binary_semaphore 实现得比 counting_semaphore 的默认实现更高效
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
41
// 全局二元信号量实例
// 设置对象计数为零
// 对象在未被发信状态
std::binary_semaphore smphSignal(0);

void ThreadProc()
{
// 通过尝试减少信号量的计数等待来自主程序的信号
smphSignal.acquire();

// 此调用阻塞直至信号量的计数被从主程序增加

std::cout << "[thread] Got the signal" << std::endl; // 回应消息

// 等待 3 秒以模仿某种线程正在进行的工作
std::this_thread::sleep_for(3s);

std::cout << "[thread] Send the signal\n"; // 消息

// 对主程序回复发信
smphSignal.release();
}

int main()
{
// 创建某个背景工作线程,它将长期存在
std::jthread thrWorker(ThreadProc);

std::cout << "[main] Send the signal\n"; // 消息

// 通过增加信号量的计数对工作线程发信以开始工作
smphSignal.release();

// release() 后随 acquire() 可以阻止工作线程获取信号量,所以添加延迟:
std::this_thread::sleep_for(50ms);

// 通过试图减少信号量的计数等待直至工作线程完成工作
smphSignal.acquire();

std::cout << "[main] Got the signal\n"; // 回应消息
}

latch

latchstd::ptrdiff_t 类型的向下计数器,它能用于同步线程。在创建时初始化计数器的值。其主要有以下特点:

  • 线程可能在 latch 上阻塞直至计数器减少到零。没有可能增加或重置计数器,这使得 latch 为单次使用的屏障
  • 同时调用 latch 的成员函数,除了析构函数,不引入数据竞争
  • 不同于 std::barrier,参与线程能减少std::latch 多于一次

barrier

类模板 barrier 提供允许至多为期待数量的线程阻塞直至期待数量的线程到达该屏障。不同于 latch,屏障可重用:一旦到达的线程从屏障阶段的同步点除阻,则可重用同一屏障。屏障对象的生存期由屏障阶段的序列组成。每个阶段定义一个阶段同步点。在阶段中到达屏障的线程能通过调用wait 在阶段同步点上阻塞,而且将保持阻塞直至运行阶段完成步骤

屏障阶段由以下步骤组成:

  • 每次调用 arrivearrive_and_drop减少期待计数
  • 期待计数抵达零时,运行阶段完成步骤。完成步骤调用完成函数对象,并除阻所有在阶段同步点上阻塞的线程。完成步骤的结束强先发生于所有从完成步骤所除阻的调用的返回
    • 对于特化 std::barrier<>(使用默认模板实参),完成步骤作为对arrivearrive_and_drop的导致期待计数抵达零的调用的一部分运行
    • 对于其他特化,完成步骤在该阶段期间到达屏障的线程之一上运行。而若在完成步骤中调用屏障对象的 wait 以外的成员函数,则行为未定义
  • 完成步骤结束时,重置期待计数为构造中指定的值,可能为 arrive_and_drop 调用所调整,并开始下一阶段

同时调用 barrier 的成员函数,除了析构函数,不引入数据竞争

位运算库

bit 库封装了一些常用的位操作。包括:

bit_cast:将一个类型的对象表示重解释为另一类型的对象表示
byteswap:反转给定整数值中的字节
has_single_bit:检查一个数是否为二的整数次幂
bit_ceil:寻找不小于给定值的最小的二的整数次幂
bit_floor:寻找不大于给定值的最大的二的整数次幂
bit_width:寻找表示给定值所需的最小位数
rotl:计算逐位左旋转的结果
rotr:计算逐位右旋转的结果
countl_zero:从最高位起计量连续的 0 位的数量
countl_one:从最高位起计量连续的 1 位的数量
countr_zero:从最低位起计量连续的 0 位的数量
countr_one:从最低位起计量连续的 1 位的数量
popcount:计量无符号整数中为 1 的位的数量

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
41
42
43
44
namespace std {
// bit_­cast
template<class To, class From>
constexpr To bit_cast(const From& from) noexcept;

// 位交换
template <class T>
constexpr T byteswap (T value) noexcept;

// 2 的整数次幂
template<class T>
constexpr bool has_single_bit(T x) noexcept;
template<class T>
constexpr T bit_ceil(T x);
template<class T>
constexpr T bit_floor(T x) noexcept;
template<class T>
constexpr T bit_width(T x) noexcept;

// 旋转
template<class T>
[[nodiscard]] constexpr T rotl(T x, int s) noexcept;
template<class T>
[[nodiscard]] constexpr T rotr(T x, int s) noexcept;

// 计数
template<class T>
constexpr int countl_zero(T x) noexcept;
template<class T>
constexpr int countl_one(T x) noexcept;
template<class T>
constexpr int countr_zero(T x) noexcept;
template<class T>
constexpr int countr_one(T x) noexcept;
template<class T>
constexpr int popcount(T x) noexcept;

// 端序
enum class endian {
little = /* 见描述 */,
big = /* 见描述 */,
native = /* 见描述 */
};
}

ranges

ranges提供处理元素范围的组件,包括各种视图适配器。其最大的作用就是让我们可以像组装函数一样组装算法,使代码更加高效、便利、可读。提供命名空间别名 std::views,作为std::ranges::views 的缩写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <ranges>
#include <iostream>

int main()
{
auto const ints = {0,1,2,3,4,5};
auto even = [](int i) { return 0 == i % 2; };
auto square = [](int i) { return i * i; };

// 组合视图的“管道”语法:
for (int i : ints | std::views::filter(even) | std::views::transform(square)) {
std::cout << i << ' ';
}

std::cout << '\n';

// 传统的“函数式”组合语法:
for (int i : std::views::transform(std::views::filter(ints, even), square)) {
std::cout << i << ' ';
}
}

C++ 20 新特性
https://silhouettesforyou.github.io/2024/07/11/2ed7adeb8fce/
Author
Xiaoming
Posted on
July 11, 2024
Licensed under