引言:C++现代化进程的里程碑
C++20 被广泛誉为自 C++11 以来最具影响力的标准,而 C++23 则在此基础上进一步打磨和完善。这两个标准共同为 C++ 带来了堪比”现代化革命”的变化——从编译期概念检查到模块化编译,从函数式风格的范围操作到协程的无栈并发,C++ 正在从一门”带类的 C”蜕变为一门兼具高性能与表达力的现代系统级语言。
本文将以实战视角,深入解析 C++20/23 中最核心的四大特性——Concepts(概念)、Modules(模块)、Ranges(范围)和 Coroutines(协程),并辅以完整的代码示例和迁移指南,帮助你在实际项目中快速落地这些新特性。
文章末尾还会介绍 C++23 中值得关注的实用改进,让你对 C++ 的未来方向有一个全面的认识。
一、Concepts:模板编程的契约约束
1.1 为什么需要 Concepts
在 C++20 之前,模板编程的错误信息堪称噩梦。当你向一个模板函数传入了不兼容的类型时,编译器会吐出几十页的模板实例化堆栈,让你在层层嵌套的模板特化中艰难寻找真正的问题所在。
Concepts 的核心思想是:在模板参数上施加约束,并使这些约束成为接口的一部分。这带来了三大好处:
- 清晰的错误信息:编译器在概念检查失败时立即报错,而非等到深层实例化
- 重载决议更精确:函数模板可以根据概念进行重载,而非依赖 SFINAE 的复杂技巧
- 自文档化:代码阅读者一眼就能看出模板参数需要满足什么条件
1.2 标准 Concepts 的使用
标准库提供了丰富的预定义概念,分布在
1 | <concepts> |
头文件中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 #include <concepts>
#include <vector>
#include <list>
#include <iostream>
// 使用 std::sortable 概念约束
template <std::sortable T>
void sort_and_print(T& container) {
std::sort(container.begin(), container.end());
for (const auto& v : container) {
std::cout << v << " ";
}
std::cout << "\n";
}
int main() {
std::vector vec{3, 1, 4, 1, 5, 9};
sort_and_print(vec); // 编译通过 ✓
// std::list lst{3, 1, 4};
// sort_and_print(lst); // 编译错误 ✗:list 不是随机访问迭代器
}
1.3 自定义 Concepts
使用
1 | concept |
关键字和
1 | requires |
表达式可以轻松定义自己的概念:
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 #include <type_traits>
#include <iostream>
#include <string>
// 定义一个序列化概念
template <typename T>
concept Serializable = requires(T t, std::ostream& os) {
{ t.serialize(os) } -> std::same_as<void>;
};
// 定义一个可反序列化概念
template <typename T>
concept Deserializable = requires(T t, std::istream& is) {
{ t.deserialize(is) } -> std::same_as<void>;
};
// 组合概念
template <typename T>
concept FullSerializable = Serializable<T> && Deserializable<T>;
// 使用概念约束
struct Config {
std::string name;
int version;
void serialize(std::ostream& os) const {
os << name << "," << version;
}
void deserialize(std::istream& is) {
char comma;
is >> name >> comma >> version;
}
};
template <FullSerializable T>
void save_and_load(T& obj, const std::string& path) {
std::ofstream ofs(path);
obj.serialize(ofs);
ofs.close();
std::ifstream ifs(path);
T loaded;
loaded.deserialize(ifs);
ifs.close();
}
int main() {
Config cfg{"app", 2};
save_and_load(cfg, "config.txt"); // ✓
// save_and_load(42, "x.txt"); // ✗ int 不满足概念
}
1.4 与 SFINAE 的对比
| 特性 | SFINAE (C++17 及以前) | Concepts (C++20) |
|---|---|---|
| 语法简洁性 | 需要 std::enable_if、decltype 等复杂技巧 | 直观的 concept + requires 语法 |
| 错误信息 | 深层模板实例化堆栈,难以定位 | 在概念检查点立即报错,信息清晰 |
| 重载能力 | 通过 SFINAE 技巧实现,但代码可读性差 | 概念重载自然直观 |
| 约束检查时机 | 模板实例化时 | 模板签名解析时(更早) |
| 代码可维护性 | 低,模板代码难以阅读 | 高,接口约束一目了然 |
二、Modules:告别头文件地狱
2.1 传统头文件的问题
头文件机制是 C++ 长期以来的痛点:
- 宏污染:头文件中的宏定义会泄漏到所有包含它的翻译单元
- 编译缓慢:每个 .cpp 文件都要独立解析所有头文件,导致大量重复工作
- 包含顺序敏感:头文件间的依赖关系微妙,一不小心就出现编译错误
- ODR 违反:定义在头文件中的实体在不同翻译单元中可能产生不一致
2.2 Module 基础语法
一个模块通常由两部分组成:模块接口单元(.cppm 或 .ixx)和模块实现单元(.cpp)。
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 // math.ixx — 模块接口单元
export module math;
// 导出子模块
export import :arithmetic;
export import :geometry;
// 内部辅助函数(不导出)
namespace detail {
constexpr double PI = 3.14159265358979323846;
}
// 导出函数
export namespace math {
double radians(double degrees) {
return degrees * detail::PI / 180.0;
}
// 导出模板
template <typename T>
T min(T a, T b) {
return a < b ? a : b;
}
}
// math_arithmetic.ixx — 子模块接口
export module math:arithmetic;
export namespace math::arithmetic {
int gcd(int a, int b) {
while (b != 0) {
int t = b;
b = a % b;
a = t;
}
return a;
}
}
1
2
3
4
5
6
7
8
9 // main.cpp — 使用模块
import math;
import <iostream>;
int main() {
std::cout << "45° in radians: " << math::radians(45) << "\n";
std::cout << "GCD(48, 18): " << math::arithmetic::gcd(48, 18) << "\n";
std::cout << "min(3, 7): " << math::min(3, 7) << "\n";
}
2.3 CMake 中的模块构建
CMake 从 3.28 版本开始实验性支持 C++20 Modules。以下是 CMakeLists.txt 的配置示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 cmake_minimum_required(VERSION 3.28)
project(ModuleDemo LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 启用模块支持
if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.28)
set(CMAKE_EXPERIMENTAL_CXX_MODULE_CMAKE_API "2182bf5c-ef0d-489a-91da-49dbc3090d2a")
set(CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDOC ON)
endif()
# 将模块接口文件添加到目标
add_library(math_lib
math.ixx
math_arithmetic.ixx
math_geometry.ixx
)
target_compile_features(math_lib PUBLIC cxx_std_20)
add_executable(demo main.cpp)
target_link_libraries(demo PRIVATE math_lib)
2.4 迁移策略
将现有项目迁移到模块并非一蹴而就的过程。以下是推荐的渐进式迁移策略:
- 从内向外:先从最底层的工具库开始迁移,这些库通常接口稳定、依赖少
- 保留头文件兼容:使用模块分区(module partitions)保留对旧头文件的兼容
- 全局模块片段:使用
1module;
前缀的全局模块片段来引入传统头文件,逐步替换
- 编译时间对比:每次迁移后测量编译时间,用数据驱动决策
根据实际项目经验,完全迁移到模块后,大型项目的增量编译时间可以减少 40%-60%。
三、Ranges:函数式风格的序列操作
3.1 从传统迭代器到 Ranges
传统的 STL 算法通过迭代器对容器进行操作,API 较为繁琐:
1
2
3
4
5
6
7
8
9 // 传统方式:找出所有偶数并平方,然后排序
std::vector<int> data = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};
std::vector<int> evens;
std::copy_if(data.begin(), data.end(), std::back_inserter(evens),
[](int n) { return n % 2 == 0; });
std::vector<int> squares;
std::transform(evens.begin(), evens.end(), std::back_inserter(squares),
[](int n) { return n * n; });
std::sort(squares.begin(), squares.end());
而使用 Ranges 可以写成声明式的管道风格:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 #include <ranges>
#include <vector>
#include <iostream>
auto data = std::vector{3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};
// Ranges 管道风格
auto result = data
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; })
| std::views::take(3); // 只取前3个
for (int n : result) {
std::cout << n << " "; // 输出: 4 16 36
}
3.2 惰性求值与视图组合
一个关键的设计特性是 惰性求值(Lazy Evaluation):视图(View)并不拷贝数据,而是在迭代时实时计算。这意味着:
- 零额外的内存分配
- 组合操作不会产生中间容器
- 可以在无限序列上操作
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 #include <ranges>
#include <iostream>
int main() {
// 生成无限的自然数序列,取出前10个偶数
auto even_numbers = std::views::iota(1) // 1, 2, 3, 4, ...
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::take(10); // 只取前10个
for (int n : even_numbers) {
std::cout << n << " "; // 2 4 6 8 10 12 14 16 18 20
}
std::cout << "\n";
// 斐波那契数列生成器
auto fibonacci = std::views::iota(0ull, 1ull)
| std::views::adjacent<2>
| std::views::transform([](auto pair) {
auto [a, b] = pair;
return a + b;
});
// 实际上我们需要更精确的斐波那契生成方式
// 使用 std::views::generate (C++23)
}
3.3 C++23 新增的 Ranges 组件
C++23 为 Ranges 库带来了多项重要补充:
| 组件 | 说明 | 示例 | ||||
|---|---|---|---|---|---|---|
|
将多个范围”拉链”合并为一个元组范围 |
|
||||
|
为每个元素附加索引 |
|
||||
|
将范围分块为固定大小的子范围 |
|
||||
|
滑动窗口视图 |
|
||||
|
按步长跳过元素 |
|
||||
|
从生成函数创建无限序列 |
|
||||
|
多个范围的笛卡尔积 |
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 // C++23 zip 示例
#include <ranges>
#include <vector>
#include <string>
#include <iostream>
int main() {
std::vector<std::string> names = {"Alice", "Bob", "Charlie"};
std::vector<int> ages = {30, 25, 35};
std::vector<double> scores = {95.5, 87.0, 92.3};
for (auto [name, age, score] : std::views::zip(names, ages, scores)) {
std::cout << name << ": " << age << " years old, score: "
<< score << "\n";
}
// enumerate 示例
for (auto [idx, item] : std::views::enumerate(names)) {
std::cout << "[" << idx << "] " << item << "\n";
}
}
四、Coroutines:无栈协程实战
4.1 协程的基础概念
C++20 的协程是无栈协程——与操作系统的线程栈无关,而是通过编译器生成的状态机在堆上保存执行状态。这意味着协程的创建和切换开销极低,百万级并发不再是梦想。
C++20 协程的三个核心关键字:
-
1co_await
:挂起协程,等待异步操作完成
-
1co_yield
:生成一个值并挂起,常用于生成器
-
1co_return
:结束协程并返回最终值
4.2 一个简单的 Generator
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
57
58
59
60
61
62
63
64
65
66 #include <coroutine>
#include <exception>
#include <iostream>
// 简易生成器框架
template <typename T>
struct Generator {
struct promise_type {
T current_value;
Generator get_return_object() {
return Generator{
std::coroutine_handle<promise_type>::from_promise(*this)
};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { std::terminate(); }
std::suspend_always yield_value(T value) {
current_value = std::move(value);
return {};
}
void return_void() {}
};
std::coroutine_handle<promise_type> handle;
explicit Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
~Generator() { if (handle) handle.destroy(); }
Generator(const Generator&) = delete;
Generator& operator=(const Generator&) = delete;
Generator(Generator&& other) noexcept : handle(other.handle) {
other.handle = nullptr;
}
bool next() {
if (!handle || handle.done()) return false;
handle.resume();
return !handle.done();
}
T value() { return handle.promise().current_value; }
};
// 生成斐波那契数列
Generator<unsigned long long> fibonacci() {
unsigned long long a = 0, b = 1;
while (true) {
co_yield b;
auto tmp = a + b;
a = b;
b = tmp;
}
}
int main() {
auto fib = fibonacci();
for (int i = 0; i < 10; ++i) {
fib.next();
std::cout << fib.value() << " ";
}
// 输出: 1 1 2 3 5 8 13 21 34 55
}
4.3 异步网络请求示例
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 #include <coroutine>
#include <thread>
#include <functional>
#include <chrono>
#include <iostream>
// 使用 cppcoro 风格的任务框架(简化版)
struct Task {
struct promise_type {
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
Task get_return_object() { return Task{}; }
void unhandled_exception() { std::terminate(); }
void return_void() {}
};
};
class AsyncTimer {
std::chrono::milliseconds duration;
public:
explicit AsyncTimer(std::chrono::milliseconds d) : duration(d) {}
bool await_ready() const { return duration.count() == 0; }
void await_suspend(std::coroutine_handle<> handle) {
std::thread([handle, dur = duration]() {
std::this_thread::sleep_for(dur);
handle.resume();
}).detach();
}
void await_resume() {}
};
Task async_example() {
std::cout << "开始异步操作...\n";
co_await AsyncTimer(std::chrono::seconds(1));
std::cout << "1秒后恢复执行\n";
co_await AsyncTimer(std::chrono::seconds(2));
std::cout << "又过了2秒\n";
}
int main() {
async_example();
std::this_thread::sleep_for(std::chrono::seconds(4));
// 输出:
// 开始异步操作...
// 1秒后恢复执行
// 又过了2秒
}
五、C++23 实用新特性一览
除了上述四大核心特性外,C++23 还带来了许多值得关注的实用改进:
5.1 std::expected — 错误处理的新范式
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 #include <expected>
#include <string>
#include <system_error>
#include <iostream>
enum class ParseError {
InvalidFormat,
OutOfRange,
EmptyInput
};
std::expected<int, ParseError> parse_int(const std::string& s) {
if (s.empty()) return std::unexpected(ParseError::EmptyInput);
try {
size_t pos;
int result = std::stoi(s, &pos);
if (pos != s.size()) return std::unexpected(ParseError::InvalidFormat);
return result;
} catch (...) {
return std::unexpected(ParseError::InvalidFormat);
}
}
int main() {
auto result = parse_int("42");
if (result) {
std::cout << "Parsed: " << *result << "\n";
}
auto err = parse_int("abc");
if (!err) {
std::cout << "Error code: " << static_cast<int>(err.error()) << "\n";
}
// 使用 and_then / or_else 链式调用
auto doubled = parse_int("21")
.and_then([](int v) -> std::expected<int, ParseError> {
return v * 2;
})
.or_else([](ParseError e) -> std::expected<int, ParseError> {
std::cerr << "Parse failed!\n";
return std::unexpected(e);
});
std::cout << "Doubled: " << *doubled << "\n"; // 42
}
5.2 std::flat_map / std::flat_set — 紧凑关联容器
基于有序向量的关联容器,适用于元素数量不大且读取频繁的场景:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 #include <flat_map>
#include <string>
#include <iostream>
int main() {
std::flat_map<int, std::string> cache;
cache.insert_or_assign(1, "One");
cache.insert_or_assign(2, "Two");
cache.insert_or_assign(3, "Three");
// 查询性能接近 std::map(二分查找)
if (auto it = cache.find(2); it != cache.end()) {
std::cout << "Found: " << it->second << "\n";
}
// 内存更紧凑,缓存友好
std::cout << "Size: " << cache.size() << "\n";
std::cout << "Memory: " << sizeof(cache) << " bytes\n";
}
5.3 std::mdspan — 多维数组视图
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 #include <mdspan>
#include <vector>
#include <iostream>
int main() {
std::vector<int> data = {
1, 2, 3, 4,
5, 6, 7, 8,
9, 10, 11, 12
};
// 将一维数组解释为 3x4 矩阵
auto mat = std::mdspan(data.data(), 3, 4);
// 访问元素
std::cout << "mat[1][2] = " << mat[1, 2] << "\n"; // 7
// 遍历
for (int i = 0; i < mat.extent(0); ++i) {
for (int j = 0; j < mat.extent(1); ++j) {
std::cout << mat[i, j] << " ";
}
std::cout << "\n";
}
}
5.4 其他值得关注的改进
| 特性 | 说明 | ||||||
|---|---|---|---|---|---|---|---|
/
|
基于 fmtlib 的类型安全格式化输出,替代
|
||||||
|
标准化的栈回溯,用于调试和异常报告 | ||||||
|
编译期检测,判断当前是否在常量求值上下文中 | ||||||
/
|
与 C 风格 API 的智能指针互操作 | ||||||
|
字节序反转,网络编程中非常实用 | ||||||
decay-copy |
显式 decay-copy 语义,简化完美转发场景 | ||||||
| 多维 operator[] | 支持
语法,
的基石 |
||||||
/
|
预处理指令的简化写法 |
六、编译器支持与迁移建议
6.1 各编译器支持状态
| 特性 | GCC | Clang | MSVC |
|---|---|---|---|
| Concepts | ✅ 10+ | ✅ 10+ | ✅ VS 2019 16.3 |
| Modules | ✅ 11+ (实验性) | ✅ 17+ (逐步完善) | ✅ VS 2019 16.10 |
| Ranges | ✅ 10+ | ✅ 13+ | ✅ VS 2019 16.10 |
| Coroutines | ✅ 10+ | ✅ 10+ | ✅ VS 2017 15.3 |
| C++23 std::expected | ✅ 12+ | ✅ 16+ | ✅ VS 2022 17.5 |
| std::flat_map | ✅ 14+ | ⚠️ 开发中 | ✅ VS 2022 17.10 |
| std::mdspan | ✅ 14+ | ⚠️ 开发中 | ✅ VS 2022 17.4 |
| std::print | ✅ 13+ | ✅ 17+ | ✅ VS 2022 17.2 |
6.2 迁移优先级建议
- 立即采用:Concepts 和 Ranges。它们对现有代码影响最小,但能显著提升代码质量和开发体验。从新写代码开始使用,逐步重构旧代码。
- 短期规划:std::expected 和 std::print。它们能显著改善错误处理和日志输出,且迁移成本极低。
- 中期规划:Coroutines。虽然学习曲线较陡,但对于 I/O 密集型应用(网络服务器、文件系统操作),协程能带来巨大的性能提升和代码简化。
- 长期规划:Modules。这是最需要基础设施支持的改动,涉及构建系统、CI/CD 工具链的全面升级。建议在大型重构或新项目启动时纳入。
七、总结
C++20/23 代表了 C++ 语言发展史上的一次范式跃迁。Concepts 让模板编程从”考古级错误信息”进化到”优雅的契约式泛型”;Modules 有望终结困扰了 C++ 数十年的头文件问题;Ranges 带来了函数式风格的声明式编程;Coroutines 则为高性能并发提供了零开销的抽象。
对于 C++ 开发者而言,现在是拥抱这些新特性的最佳时机——所有主流编译器已经提供了完善的 C++20 支持,C++23 的核心特性也已大规模可用。从今天开始,在你的下一个项目中启用
1 | -std=c++20 |
或
1 | -std=c++23 |
,体验现代 C++ 带来的开发效率提升吧。
正如 Bjarne Stroustrup 所说:”C++ 的目标是让程序员生活得更轻松,让复杂软件更可靠。”C++20/23 正在将这个愿景变为现实。

Photo by AltumCode on Unsplash
汤不热吧