第 10 章 C++20

C++20 如同 C++11 一样,似乎能够成为一个振奋人心的更新。例如,早在 C++11 时期就跃跃欲试呼声极高却最终落选的 Concept,如今已经箭在弦上。 C++ 组委会在讨论投票最终确定 C++20 有很多提案,诸如 Concepts/Module/Coroutine/Ranges/ 等等。 本章我们就来一览 C++20 即将引入的那些重要特性。

概念与约束

概念(Concepts)是对 C++ 模板编程的进一步增强扩展。简单来说,概念是一种编译期的特性, 它能够让编译器在编译期时对模板参数进行判断,从而大幅度增强我们在 C++ 中模板编程的体验。 使用模板进行编程时候我们经常会遇到各种令人发指的错误, 这是因为到目前为止我们始终不能够对模板参数进行检查与限制。 举例而言,下面简单的两行代码会造成大量的几乎不可读的编译错误:

#include <list>
#include <algorithm>
int main() {
std::list<int> l = {1, 2, 3};
std::sort(l.begin(), l.end());
return 0;
}

而这段代码出现错误的根本原因在于,std::sort 对排序容器必须提供随机迭代器,否则就不能使用,而我们知道 std::list 是不支持随机访问的。 用概念的语言来说就是:std::list 中的迭代器不满足 std::sort 中随机迭代器这个概念的约束(Constraint)。 在引入概念后,我们就可以这样对模板参数进行约束:

template <typename T>
requires Sortable<T> // Sortable 是一个概念
void sort(T& c);

缩写为:

template<Sortable T> // T 是一个 Sortable 的类型名
void sort(T& c)

甚至于直接将其作为类型来使用:

void sort(Sortable& c); // c 是一个 Sortable 类型的对象

我们现在来看一个实际的例子。下面用 requires 表达式定义一个概念 Addable,要求类型支持 + 运算且结果可转换回该类型,并用它来约束函数模板:

#include <concepts>
#include <iostream>

// 定义概念:T 必须支持 a + b,且结果可转换为 T
template <typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>;
};

template <Addable T>
T sum(T a, T b) { return a + b; }

int main() {
std::cout << sum(1, 2) << std::endl; // 3
std::cout << sum(1.5, 2.5) << std::endl; // 4
// sum("a", "b"); // 编译错误:不满足 Addable,错误信息清晰明了
}

当传入不满足约束的类型时,编译器会直接告诉我们「约束不满足」,而不是抛出一长串模板实例化的内部错误。

模块

模块(Modules)旨在解决传统头文件机制带来的诸多问题:重复解析、宏污染、包含顺序敏感以及缓慢的编译速度。一个模块用 export module 声明,并显式地 export 对外可见的实体:

// math.cppm —— 模块接口单元
export module math;

export int add(int a, int b) {
return a + b;
}

使用方则用 import 代替 #include

// main.cpp
import math;
import <iostream>;

int main() {
std::cout << add(1, 2) << std::endl;
}

与本书其他示例不同,模块的编译需要工具链的专门支持,并且通常要分两步(先编译模块接口单元、再编译使用方),无法用 clang++ file.cpp 这样的单条命令直接构建。具体的构建方式请参阅你所用编译器的文档。

范围

范围(Ranges)为标准库的算法与迭代器提供了更高层、可组合的抽象。借助范围适配器 (range adaptors) 与管道运算符 |,我们可以把多个惰性变换串联起来,代码更具声明式风格:

#include <iostream>
#include <vector>
#include <ranges>

int main() {
std::vector<int> v{1, 2, 3, 4, 5, 6};
auto result = v | std::views::filter([](int x) { return x % 2 == 0; })
| std::views::transform([](int x) { return x * x; });
for (int x : result) std::cout << x << ' '; // 4 16 36
std::cout << std::endl;
}

这些视图都是惰性求值的:只有在真正遍历 result 时,过滤与变换才会逐元素地被计算,中间不会产生临时容器。

协程

协程(Coroutines)是可以被挂起与恢复的函数。一个函数只要在函数体中使用了 co_awaitco_yieldco_return,它就是一个协程。需要强调的是,C++20 只提供了协程的语言机制以及 <coroutine> 中的底层支撑设施,而把 promise_type 这类「胶水」留给用户或库去实现(C++23 才提供了开箱即用的 std::generator)。

下面是一个最小的惰性生成器,利用 co_yield 逐个产出值:

#include <coroutine>
#include <iostream>
#include <optional>

template <typename T>
struct Generator {
struct promise_type {
T current;
Generator get_return_object() { return Generator{handle::from_promise(*this)}; }
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(T value) { current = value; return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
using handle = std::coroutine_handle<promise_type>;
handle h;
explicit Generator(handle h) : h(h) {}
~Generator() { if (h) h.destroy(); }
Generator(const Generator&) = delete;
Generator(Generator&& o) noexcept : h(o.h) { o.h = {}; }
std::optional<T> next() {
if (!h || h.done()) return std::nullopt;
h.resume();
if (h.done()) return std::nullopt;
return h.promise().current;
}
};

Generator<int> range(int a, int b) {
for (int i = a; i < b; ++i) co_yield i;
}

int main() {
auto g = range(1, 5);
while (auto v = g.next()) std::cout << *v << ' '; // 1 2 3 4
std::cout << std::endl;
}

关于合约与事务内存

需要澄清一个常见的误解:合约 (Contracts)事务内存 (Transactional Memory) 并不属于 C++20。

因此,本章不再将它们作为 C++20 的特性来介绍。

总结

总的来说,终于在 C++20 中看到 Concepts/Ranges/Modules 这些令人兴奋的特性, 这对于一门已经三十多岁『高龄』的编程语言,依然是充满魅力的。

进一步阅读的参考资料