一、概述
C++23(ISO/IEC 14882:2024)是继 C++20 之后的又一次重要更新。如果说 C++20 是"四大天王"级别的革命性升级,那么 C++23 就是"填坑 + 完善 + 实用化"的版本——它补齐了 C++20 遗留的短板(如 std::format 没有 print),引入了期待已久的 std::expected 错误处理方案,并添加了大量让日常编码更愉快的便利特性。
C++23 核心看点(一句话总结)
最重要的std::print/println —— 终于告别 std::cout 的冗长语法
最实用的std::expected<T, E> —— 不用异常也能优雅处理错误
最惊喜的Deducing this —— CRTP 的终结者,Lambda 递归的救星
最期待的std::flat_map / std::flat_set —— 缓存友好的扁平容器
最养眼的std::mdspan —— 多维数组视图,科学计算的福音
最爽快的optional::and_then / transform —— 链式调用消灭嵌套 if
目录
- 概述与版本定位
- std::print / std::println —— 终于能打印了
- std::expected —— 错误处理新范式
- std::optional 单子操作 —— 链式调用
- std::flat_map / std::flat_set —— 扁平容器
- Deducing this —— 显式对象参数
- std::mdspan —— 多维跨度
- std::generator —— 协程生成器
- std::ranges::to —— 容器转换
- if consteval —— 编译期判断
- 多维下标运算符 a[1, 2]
- static operator() / operator[]
- std::move_only_function
- std::stacktrace —— 堆栈跟踪
- 其他实用特性合集
- 总结与编译器支持
二、std::print / std::println —— 终于能打印了
C++20 引入了 std::format,带来了 Python f-string 风格的格式化能力。但令人抓狂的是——它只能格式化字符串,不能直接输出到终端!你必须写:
// C++20:又臭又长 #include <format> #include <iostream> std::cout << std::format("Hello, {}! You are {} years old.", name, age); // 或者先 format 再 cout,多此一举 auto s = std::format("x={}, y={}", x, y); std::cout << s << '\n';
C++23 终于补上了这个坑:std::print 和 std::println 直接格式化并输出,无需 #include <iostream>,无需手动加换行。
#include <print> // 就这一个头文件 // 基本用法 std::println("Hello, {}!"); // Hello, world! std::println("Name: {}, Age: {}", name, age); // Name: Alice, Age: 30 // print 不换行,println 换行 std::print("Loading... "); std::print("{}%", progress); std::println(); // 空参数 = 只输出换行 // 格式化说明符:和 std::format 完全一致 std::println("Pi = {:.2f}", 3.14159); // Pi = 3.14 std::println("Hex: {:08x}", 255); // Hex: 000000ff std::println("{:>20}", "right aligned"); // 右对齐,宽度20 // 输出到文件 std::FILE* fp = std::fopen("log.txt", "w"); std::println(fp, "Log: {} = {}", key, value);
· 性能:println 内部使用编译期格式字符串解析,比 cout 的 << 链式调用快得多
· 可读性:
println("x={}, y={}", x, y) 比 cout << "x=" << x << ", y=" << y << '\n' 清晰十倍· 类型安全:编译期检查格式字符串与参数类型匹配
· 国际化:格式字符串中的占位符顺序可以调整,cout 链做不到
三、std::expected<T, E> —— 错误处理新范式
C++ 的错误处理一直有两派之争:异常 vs 返回码。异常优雅但开销不可预测;返回码高效但容易忘记检查。C++23 引入的 std::expected<T, E> 借鉴了 Rust 的 Result<T, E>,提供了一种既安全又高效的方案。
核心思想:函数要么返回期望的值 T,要么返回错误 E。调用者必须处理两种情况,不能"假装没看到"。
#include <expected> #include <string> // 定义错误类型 enum class ParseError { Empty, InvalidChar, OutOfRange }; // 返回 expected:成功返回 int,失败返回 ParseError std::expected<int, ParseError> parse_int(const std::string& s) { if (s.empty()) return std::unexpected(ParseError::Empty); for (char c : s) { if (!std::isdigit(c)) return std::unexpected(ParseError::InvalidChar); } long val = std::stol(s); if (val < INT_MIN || val > INT_MAX) return std::unexpected(ParseError::OutOfRange); return (int)val; // 隐式构造 expected<int, ...> } // ---- 使用示例 ---- int main() { auto result = parse_int("42"); // 方式1:has_value() + value() if (result.has_value()) { std::println("Parsed: {}", result.value()); } else { std::println("Error: {}", (int)result.error()); } // 方式2:value_or() 提供默认值 auto val = parse_int("abc").value_or(0); std::println("Value or default: {}", val); // 方式3:and_then() 链式调用(见下文 optional 详解) auto doubled = parse_int("21") .and_then([](int v) -> std::expected<int, ParseError> { return v * 2; }); std::println("Doubled: {}", doubled.value()); }
实战场景:文件读取 + JSON 解析 + 类型转换
// 链式调用,任一步失败则短路返回错误 std::expected<User, Error> load_user(const std::string& path) { return read_file(path) // expected<string, Error> .and_then(parse_json) // expected<Json, Error> .and_then(json_to_user) // expected<User, Error> .transform([](User u) { // 转换成功值 u.loaded_at = std::time(nullptr); return u; }); } // 使用:简洁且安全 if (auto user = load_user("config.json"); user) { std::println("User: {}", user->name); } else { std::println("Failed: {}", user.error().message()); }
| 方案 | 性能 | 安全性 | 可读性 | 适用场景 |
|---|---|---|---|---|
| 异常 (try/catch) | 差(栈展开开销) | 一般(可能忘记 catch) | 好 | 真正的异常情况(罕见错误) |
| 返回码 (int/enum) | 好 | 差(容易忽略) | 差 | 简单函数 |
| std::expected | 好(无异常开销) | 好(必须显式处理) | 好(链式调用) | 可预期的错误(解析失败、文件不存在等) |
四、std::optional 单子操作 —— 链式调用消灭嵌套 if
C++17 引入了 std::optional,但用它处理链式操作时,代码会陷入"嵌套 if 地狱":
// C++17 的噩梦:嵌套 if 检查 optional std::optional<Config> get_config(); std::optional<Database> get_db(const Config&); std::optional<User> get_user(const Database&, int id); auto cfg = get_config(); if (!cfg) return std::nullopt; auto db = get_db(*cfg); if (!db) return std::nullopt; auto user = get_user(*db, 42); if (!user) return std::nullopt; // 终于拿到了 user... return user;
C++23 为 optional 引入了三个单子操作(Monadic Operations):and_then、transform、or_else。一行搞定:
// C++23:链式调用,任一步返回 nullopt 则短路 auto user = get_config() .and_then([](const Config& c) { return get_db(c); }) .and_then([](const Database& db) { return get_user(db, 42); }); // transform:对成功值做变换(类似 map) auto name = get_config() .and_then([](const Config& c) { return get_db(c); }) .and_then([](const Database& db) { return get_user(db, 42); }) .transform([](const User& u) { return u.name; }); // optional<string> // or_else:失败时提供替代方案 auto safe_user = get_config() .and_then(get_db) .and_then([](auto& db) { return get_user(db, 42); }) .or_else([] { return std::optional{User::guest()}; }); // 失败就用默认用户
三个操作的对比
and_then 接收一个返回 optional<U> 的函数,用于链式串联多个可能失败的操作
transform 接收一个返回普通值 U 的函数,用于变换成功值(类似 map)
or_else 接收一个返回 optional<T> 的函数,用于兜底处理失败情况
std::expected 也有完全相同的三个操作(and_then / transform / or_else),用法一致。这意味着 optional 和 expected 可以用同样的链式风格编程,心智负担极低。
五、std::flat_map / std::flat_set —— 扁平容器
传统的 std::map 和 std::set 底层是红黑树,每个节点单独分配内存,缓存不友好。C++23 引入了扁平版本,底层用连续数组存储,牺牲插入/删除时间换取大幅提升的遍历和查找性能。
#include <flat_map> #include <flat_set> #include <print> std::flat_map<std::string, int> scores; // API 和 std::map 完全一致 scores["Alice"] = 95; scores["Bob"] = 87; scores.emplace("Charlie", 92); // 遍历(内存连续,缓存命中率高,快得多) for (const auto& [name, score] : scores) { std::println("{}: {}", name, score); } // 查找:O(log n) 二分查找,但常数因子远小于红黑树 auto it = scores.find("Alice"); if (it != scores.end()) { std::println("Found: {}", it->second); } // flat_set 同理 std::flat_set<int> nums = {3, 1, 4, 1, 5, 9, 2, 6}; std::println("Contains 5: {}", nums.contains(5));
| 特性 | std::map (红黑树) | std::flat_map (有序数组) |
|---|---|---|
| 底层结构 | 红黑树(节点离散分配) | 两个有序 vector(key 数组 + value 数组) |
| 查找 | O(log n) | O(log n)(二分查找,但缓存友好) |
| 遍历 | O(n),缓存不友好 | O(n),缓存友好,快 5-10 倍 |
| 插入/删除 | O(log n) | O(n)(需要移动元素) |
| 内存开销 | 每节点 3 指针 + 颜色 | 紧凑连续存储 |
| 适用场景 | 频繁插入删除 | 读多写少、数据量不大、需要遍历 |
六、Deducing this —— 显式对象参数
C++23 最具创新性的语言特性之一。允许在成员函数的第一个参数位置写 this,编译器会自动推导对象的 cv 限定符、值类别(lvalue/rvalue)。
6.1 消灭 CRTP
// ---- C++20 之前:CRTP(奇异递归模板模式)---- template<typename Derived> struct Base { void interface() { static_cast<Derived*>(this)->impl(); // 丑陋的强制转换 } }; struct Concrete : Base<Concrete> { void impl() { std::println("Concrete::impl"); } }; // ---- C++23:Deducing this,干净利落 ---- struct Base { template<typename Self> void interface(this Self& self) { // Self 自动推导为 Concrete self.impl(); // 直接调用,无需转换 } }; struct Concrete : Base { void impl() { std::println("Concrete::impl"); } };
6.2 一个函数替代四个重载
// ---- C++20:要写 4 个重载来覆盖所有 cv/ref 组合 ---- struct Widget { void process() & { println("lvalue"); } void process() const & { println("const lvalue"); } void process() && { println("rvalue"); } void process() const && { println("const rvalue"); } }; // ---- C++23:一个模板函数搞定 ---- struct Widget { template<typename Self> void process(this Self&& self) { if constexpr (std::is_const_v<std::remove_reference_t<Self>>) { println("const "); } if constexpr (std::is_lvalue_reference_v<Self>) { println("lvalue"); } else { println("rvalue"); } } };
6.3 Lambda 递归
// C++20:Lambda 不能递归,必须用 std::function 或 Y组合子 // C++23:传 this 参数,Lambda 可以直接递归! auto factorial = [](this auto& self, int n) -> int { return n <= 1 ? 1 : n * self(n - 1); }; std::println("5! = {}", factorial(5)); // 120 // 更复杂的例子:递归遍历 JSON 树 auto print_json = [](this auto& self, const Json& j, int indent = 0) -> void { if (j.is_object()) { for (const auto& [k, v] : j.items()) { std::println("{:>{}}{}:", "", indent, k); self(v, indent + 2); // 递归调用! } } else if (j.is_array()) { for (const auto& elem : j) { self(elem, indent + 2); } } else { std::println("{:>{}}{}", "", indent, j.dump()); } };
七、std::mdspan —— 多维跨度
C++20 的 std::span 是一维数组视图的利器,但不能处理多维数据。C++23 引入 std::mdspan(多维 span),终于可以优雅地操作矩阵、张量了。
#include <mdspan> #include <print> // 一维数组存储的 3x4 矩阵 int data[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }; // 创建 3x4 的二维视图(不拷贝数据!) std::mdspan mat(data, 3, 4); // 用多维下标访问(C++23 多维 a[i, j] 语法) std::println("mat[1, 2] = {}", mat[1, 2]); // 第2行第3列 = 7 // 遍历二维矩阵 for (size_t i = 0; i < mat.extent(0); i++) { for (size_t j = 0; j < mat.extent(1); j++) { std::print("{:4d}", mat[i, j]); } std::println(); } // 三维张量也行 float tensor_data[24]; std::mdspan tensor(tensor_data, 2, 3, 4); // 2x3x4 三维张量 tensor[1, 2, 3] = 3.14f; // 支持非连续布局(行优先 / 列优先 / 自定义步长) using layout_right = std::layout_right; // 行优先(C 风格,默认) using layout_left = std::layout_left; // 列优先(Fortran 风格)
八、std::generator —— 协程生成器
C++20 引入了协程,但标准库没有提供现成的生成器类型,你得自己写或者用第三方库(如 cppcoro)。C++23 终于提供了 std::generator。
#include <generator> #include <print> // 生成斐波那契数列 std::generator<int> fibonacci() { int a = 0, b = 1; while (true) { co_yield a; int next = a + b; a = b; b = next; } } // 使用 int main() { // 取前 10 个斐波那契数 int count = 0; for (int val : fibonacci()) { std::print("{} ", val); if (++count >= 10) break; } std::println(); // 输出: 0 1 1 2 3 5 8 13 21 34 } // 生成器可以嵌套引用(递归 yield) std::generator<int> flatten_tree(const Node* node) { if (!node) co_return; co_yield node->value; co_yield std::ranges::elements_of(flatten_tree(node->left)); co_yield std::ranges::elements_of(flatten_tree(node->right)); }
九、std::ranges::to —— 容器转换
C++20 的 Ranges 管道很优雅,但最后想把结果存到容器里时,只能写循环或用 std::vector<...>(begin, end)。C++23 的 ranges::to 一行搞定。
#include <ranges> #include <vector> #include <string> #include <set> #include <print> int main() { std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // C++20:管道操作后,手动构造容器 std::vector<int> evens; std::ranges::copy(nums | std::views::filter([](int n) { return n % 2 == 0; }), std::back_inserter(evens)); // C++23:ranges::to 一行搞定 auto evens2 = nums | std::views::filter([](int n) { return n % 2 == 0; }) | std::ranges::to<std::vector>(); // 转成其他容器类型 auto even_set = nums | std::views::filter([](int n) { return n % 2 == 0; }) | std::ranges::to<std::set>(); // 链式 transform + to std::vector<std::string> names = {"alice", "bob", "charlie"}; auto upper = names | std::views::transform([](const std::string& s) { std::string result; for (char c : s) result += std::toupper(c); return result; }) | std::ranges::to<std::vector>(); // upper = {"ALICE", "BOB", "CHARLIE"} }
十、if consteval —— 编译期判断
C++20 引入了 std::is_constant_evaluated(),但写起来啰嗦且容易误用。C++23 提供了更直观的 if consteval 语法。
// C++20:std::is_constant_evaluated() constexpr double sqrt_approx(double x) { if (std::is_constant_evaluated()) { // 编译期:用简单的牛顿迭代法 double guess = x / 2; for (int i = 0; i < 20; i++) { guess = (guess + x / guess) / 2; } return guess; } else { // 运行期:调用标准库的 sqrt return std::sqrt(x); } } // C++23:if consteval 更清晰 constexpr double sqrt_approx(double x) { if consteval { // 编译期路径 double guess = x / 2; for (int i = 0; i < 20; i++) { guess = (guess + x / guess) / 2; } return guess; } else { // 运行期路径 return std::sqrt(x); } } constexpr double compile_time_val = sqrt_approx(2.0); // 编译期计算 double runtime_val = sqrt_approx(2.0); // 运行期调用 std::sqrt
十一、多维下标运算符 a[1, 2]
C++23 允许 operator[] 接收多个参数,终于可以写 matrix[1, 2] 而不是 matrix(1, 2) 或 matrix[1][2]。
struct Matrix { std::vector<double> data; int rows, cols; double& operator[](size_t i, size_t j) { // C++23 多维下标 return data[i * cols + j]; } const double& operator[](size_t i, size_t j) const { return data[i * cols + j]; } }; Matrix m(3, 4); m[1, 2] = 3.14; // 多维下标,清晰简洁 std::println("{}", m[1, 2]);
十二、static operator() / static operator[]
C++23 允许将 operator() 和 operator[] 声明为 static,适用于不需要访问 this 的场景,减少开销。
struct Adder { static int operator()(int a, int b) { // 不需要 this 指针 return a + b; } }; Adder add; std::println("{}", add(3, 4)); // 7 // 好处:编译器不需要传递 this 指针,生成的代码更高效 // 特别适合用作 STL 算法的谓词 std::vector<int> nums = {3, 1, 4, 1, 5}; std::sort(nums.begin(), nums.end(), Adder{}); // 无状态比较器
十三、std::move_only_function
std::function 要求目标可拷贝,这在包装只移动类型(如 unique_ptr、协程句柄)时很麻烦。C++23 引入 move_only_function 解决这个问题。
#include <functional> #include <memory> // std::function 拷贝语义,无法持有只移动对象 // std::move_only_function 移动语义,可以! std::move_only_function<int()> task; // 可以捕获 unique_ptr auto ptr = std::make_unique<int>(42); task = [p = std::move(ptr)]() { return *p; }; std::println("{}", task()); // 42 // 适合异步回调、协程回调等场景 auto callback = [promise = std::move(some_promise)]() mutable { promise.set_value(100); };
十四、std::stacktrace —— 堆栈跟踪
终于!C++ 标准库内置了堆栈跟踪功能,不再需要依赖 Boost.Stacktrace 或平台 API。
#include <stacktrace> #include <print> void deep_function() { // 获取当前调用栈 auto trace = std::stacktrace::current(); // 打印堆栈 std::println("{}", trace); // 也可以遍历每一帧 for (const auto& entry : trace) { std::println(" {} at {}:{}", entry.description(), entry.source_file(), entry.source_line()); } } void middle_function() { deep_function(); } int main() { middle_function(); } /* 输出: 0# deep_function() at app.cpp:5 1# middle_function() at app.cpp:15 2# main() at app.cpp:16 ... */
十五、其他实用特性合集
15.1 size_t 字面量后缀 uz / UZ
// C++23 前:0 是 int,可能导致隐式转换警告 for (size_t i = 0; i < vec.size(); i++) { ... } // C++23:uz 后缀直接得到 size_t for (size_t i = 0uz; i < vec.size(); i++) { ... } auto n = 100uz; // 类型为 size_t
15.2 #warning 预处理指令
// C++23 终于标准化了 #warning #warning "This header is deprecated, use new_api.hpp instead" // 以及 #elifdef / #elifndef #ifdef DEBUG #elifdef RELEASE // release 模式 #elifndef TESTING // 非 testing 模式 #endif
15.3 auto(x) / auto{x} 衰减拷贝
// C++23:auto(x) 对 x 做衰减拷贝 // 类似 C 语言的 "to decay or not to decay" 问题解决方案 std::vector<std::string> names = {"Alice", "Bob"}; // 避免悬空引用:auto(x) 安全拷贝 for (auto& name : names) { // 如果 names 可能被修改,用 auto(name) 拷贝一份安全的副本 auto safe_copy = auto(name); // decay-copy std::println("{}", safe_copy); }
15.4 std::byteswap
#include <bit> // 字节序转换,网络编程常用 uint32_t host_val = 0x12345678; uint32_t net_val = std::byteswap(host_val); std::println("{:08x}", net_val); // 78563412 // 条件字节序转换 if constexpr (std::endian::native == std::endian::little) { val = std::byteswap(val); // 小端转大端 }
15.5 range-based for 循环中的初始化语句
// C++23:range-based for 可以带初始化语句 for (auto& vec = get_data(); auto& item : vec) { std::println("{}", item); } // vec 的生命周期限于 for 循环,避免悬空引用
15.6 std::span 的 construction 改进
// C++23:span 可以从 array 构造,更灵活 std::span<const int> s = {1, 2, 3}; // 从初始化列表构造
15.7 constexpr 增强
// 更多 STL 函数变成 constexpr constexpr bool is_sorted(std::vector<int> v) { std::sort(v.begin(), v.end()); // C++23: sort 是 constexpr! return std::is_sorted(v.begin(), v.end()); }
十六、总结与编译器支持
C++23 核心特性速查表
| 特性 | 类别 | 一句话描述 | 实用度 |
|---|---|---|---|
| std::print/println | 标准库 | 终于可以直接格式化打印了 | ★★★★★ |
| std::expected | 标准库 | 不用异常也能优雅处理错误 | ★★★★★ |
| optional 单子操作 | 标准库 | and_then/transform 链式调用 | ★★★★★ |
| Deducing this | 语言 | 消灭 CRTP,Lambda 可递归 | ★★★★☆ |
| std::flat_map/set | 标准库 | 缓存友好的扁平容器 | ★★★★☆ |
| std::mdspan | 标准库 | 多维数组视图 | ★★★★☆ |
| std::generator | 标准库 | 协程生成器终于来了 | ★★★★☆ |
| ranges::to | 标准库 | Ranges 结果转容器 | ★★★★☆ |
| if consteval | 语言 | 编译期判断更清晰 | ★★★☆☆ |
| 多维下标 a[i,j] | 语言 | 矩阵操作更自然 | ★★★☆☆ |
| static operator() | 语言 | 无状态调用更高效 | ★★★☆☆ |
| move_only_function | 标准库 | 可移动的 function | ★★★☆☆ |
| std::stacktrace | 标准库 | 标准堆栈跟踪 | ★★★☆☆ |
| uz 字面量 | 语言 | size_t 后缀 | ★★☆☆☆ |
| #warning | 预处理 | 编译期警告 | ★★☆☆☆ |
· GCC 14+:支持大部分 C++23 特性(print、expected、optional 单子操作、flat_map、deducing this 等)
· Clang 18+:支持大部分语言特性,部分库特性需 libc++ 最新版
· MSVC 19.40+ (VS 2022 17.10+):支持大部分特性
· 编译时加
-std=c++23(GCC/Clang)或 /std:c++latest(MSVC)· 建议:生产环境使用 GCC 14+ 或 Clang 18+ 以获得最佳 C++23 支持
1. 立刻用起来:std::print/println、optional 单子操作、ranges::to 几乎零学习成本,立刻提升代码质量
2. 逐步引入:std::expected 替代部分异常场景,flat_map 替代读多写少的 map
3. 深入学习:Deducing this 是范式级改变,值得花时间掌握
4. 关注生态:C++23 的 std::generator 和 mdspan 会推动协程和科学计算生态发展
C++23 虽然没有 C++20 那么革命性,但它是 C++20 的完美补充——填补了 std::format 没有 print 的遗憾、提供了 expected 解决错误处理之争、用 deducing this 简化了 CRTP。这些改进让现代 C++ 的开发体验更接近 Python 和 Rust,同时保持了零开销抽象的核心优势。