C++23 是"每三年一版"节奏下的最新标准,已由 ISO 正式发布。它没有 C++20 那样的大爆炸,但带来的都是实打实的便利:错误处理有了正统的 std::expected,成员函数可以显式声明 this 参数,输出有了 std::print, ranges 库也补上了 zip 和 to。本文带你预览这些前沿特性,并说明哪些值得立刻用起来。
1. std::expected:函数式错误处理
C++20 的 std::optional 只回答"有没有值",回答不了"为什么失败"。std::expected<T, E> 类似 Rust 的 Result:要么持有成功值 T,要么持有错误 E,错误信息随值一起返回,不再依赖异常、哨兵值或出参。对"错误是常态"的代码(解析、校验、网络请求),它比异常更轻、比返回码更安全。
#include <expected>
#include <iostream>
#include <string>
enum class Error { BadFormat, OutOfRange };
// 解析字符串为整数:成功返回 int,失败返回 Error
std::expected<int, Error> parse_int(const std::string& s) {
try {
std::size_t pos = 0;
int value = std::stoi(s, &pos);
if (pos != s.size()) return std::unexpected(Error::BadFormat);
return value;
} catch (...) {
return std::unexpected(Error::BadFormat);
}
}
int main() {
auto ok = parse_int("42");
auto bad = parse_int("abc");
std::cout << (ok ? *ok : -1) << std::endl; // 42
std::cout << (bad ? *bad : -1) << std::endl; // -1
if (!bad) {
std::cout << "错误码: "
<< static_cast<int>(bad.error()) << std::endl;
}
return 0;
}
访问接口和 optional 几乎一一对应:operator* 解引用、value() 无值时抛 bad_expected_access、value_or 给默认值;错误侧用 error() 读取。配合 and_then / transform / or_else 等成员函数,还能把多个可能失败的步骤链式串起来,写出一段没有 if 的流水线。expected 最迷人的就是这种组合能力:auto r = step1().and_then(step2).or_else(handle_error); 每一步失败都自动短路,错误处理变成一条可读的链。与异常相比,它把"错误路径"显式写进类型系统,调用者想忽略都难;与返回码相比,它强制你处理错误,不会"忘了检查"。代价是代码里要多写一些类型标注,但换来的是确定性。
2. deducing this:this 作为显式参数
过去成员函数的 const 重载要写两遍:void f() 和 void f() const,代码重复且容易漏。deducing this 允许把"对象自身"作为第一个参数显式写出,编译器自动推导 const/非 const、左值/右值版本,一份代码通吃所有情况。它还让递归 lambda 第一次变得优雅。
#include <iostream>
struct Counter {
int count = 0;
// 显式对象参数:自动适配 const/非 const 调用
int get(this const Counter& self) { return self.count; }
Counter& inc(this Counter& self) {
++self.count;
return self;
}
};
int main() {
Counter c;
c.inc().inc(); // 链式调用
std::cout << c.get() << std::endl; // 2
const Counter cc = c;
std::cout << cc.get() << std::endl; // const 对象也能调用
return 0;
}
另一个经典场景是递归 lambda:auto dfs = [](this auto&& self, int n) -> void { if (n > 0) self(n - 1); };。以前递归 lambda 要用 std::function 或手动包装,现在一句声明即可,性能还更好。deducing this 还让 CRTP(奇异递归模板)这种绕弯的写法有了更直接的替代:基类模板想访问派生类成员,以前要靠 static_cast<Derived&>(*this),现在直接写成员函数参数 this Derived& self,编译器自动完成推导。标准库也在跟进,很多原本需要 const 重载的接口,未来都会统一成一份声明。
3. std::print 与 std::println:告别 cout 流式
printf 不安全,cout 啰嗦。C++23 的 std::println 兼具两者优点:格式化占位符清晰、类型安全、自动换行,还支持 {} 占位符(类似 Python 的 f-string)。日常调试和日志输出,它都是更舒服的选择。
#include <print>
int main() {
std::println("Hello, C++23!");
std::println("圆周率约等于 {:.2f}", 3.14159);
std::println("{} 年发布了 C++{}", 2023, 23);
return 0;
}
格式化语法与 std::format 完全一致:冒号后面跟格式说明,{:.2f} 表示保留两位小数的浮点数,{} 按参数顺序填充。编译期还会校验占位符与参数是否匹配,写错直接编译失败,而不是运行时才崩。std::print 输出到 stdout,std::println 自动换行,还有带 FILE* 的 std::print(FILE*, ...) 重载可以写文件。它底层基于 std::format,格式化规则与 Python 的 format 相近,学一次到处用。注意:print 系列需要编译器在编译期解析格式字符串,GCC 14 之前的版本会报错,升级工具链是使用它的前提。
4. ranges 补完:zip、to 与 optional 的 monadic 操作
C++23 给 ranges 库补上了期待已久的拼装件:std::views::zip 并行遍历多个容器,std::ranges::to 把视图直接收集成容器;std::optional 也新增 transform / and_then / or_else,链式处理"可能为空"的值,风格与 expected 对齐。
#include <optional>
#include <print>
#include <ranges>
#include <string>
#include <vector>
int main() {
// zip:并行遍历两个容器,结构化绑定拆开
std::vector<int> ids = {1, 2, 3};
std::vector<std::string> names = {"Alice", "Bob", "Carol"};
for (auto [id, name] : std::views::zip(ids, names)) {
std::println("{}: {}", id, name);
}
// ranges::to:视图直接收集成 vector
auto doubled = std::views::iota(1, 6)
| std::views::transform([](int x) { return x * 2; })
| std::ranges::to<std::vector<int>>();
// optional 链式变换
std::optional<int> o = 21;
auto msg = o.transform([](int v) { return v * 2; })
.transform([](int v) { return "答案是 " + std::to_string(v); });
std::println("{}", msg.value_or("无值"));
return 0;
}
这几样组合起来,"取数据、变换、收集、处理可能为空"整条链都能写成表达式,中间不出现一个临时变量,可读性和可维护性都远超手写循环。
5. 如何在编译器中启用 C++23
# 编译并运行(std::print 需要 GCC 14+)
g++ -std=c++23 main.cpp -o main
./main
# Clang 建议 17+,配合 libc++
clang++ -std=c++23 -stdlib=libc++ main.cpp -o main
C++23 特性在各大编译器中的落地进度不一,编译前先对照下表确认:
| 特性 | 作用 | GCC | Clang(libc++) |
|---|---|---|---|
| std::expected | 携带错误信息的返回值 | 13+ | 16+ |
| deducing this | 显式 this 参数 | 14+ | 17+ |
| std::print / println | 类型安全格式化输出 | 14+ | 18+ |
| views::zip / ranges::to | ranges 拼装件 | 13+ | 17+ |
| optional monadic | 链式处理可选值 | 13+ | 17+ |
表中没有列出的特性还有 std::stacktrace(程序崩溃时打印调用栈)、std::mdspan(多维数组视图)、static operator[] 等,它们各有用途,但对大多数项目优先级不高,知道存在即可,等真正需要时再查文档。最后提醒一点:C++23 的许多特性单独看只是"语法糖",但组合起来效果惊人——expected 加 optional 的 monadic 接口、print 加 format 的编译期检查、zip 加结构化绑定,每一条都能直接减少一类 bug。拥抱新标准最好的方式,就是在一个小项目里完整地用一遍。
6. 升级到 C++23 值不值
如果你是个人项目或新项目,值得直接上:C++23 对 C++20 完全向后兼容,新代码用 std::print 和 std::expected 写起来明显更舒服,编译期错误也更友好。但如果是团队生产环境,建议先看两个因素:一是编译器/CI 镜像是否已升级到 GCC 14+ 或 Clang 17+,二是团队是否愿意消化新语法带来的代码风格变化。再给一个判断标准:如果你们的 CI 已经在跑 C++20,那么升级 C++23 只是把 -std=c++20 改成 -std=c++23 再修少量编译错误的事;如果连 C++20 都没上,建议先补 C++20 的 concepts 和 ranges,再谈 C++23。步子太大会让团队消化不良,循序渐进反而更快。
务实的做法是分两步走:第一步,把 std::print、views::zip 这类零风险特性立刻用起来,它们不改变任何既有设计;第二步,把 expected 引入到"错误是常态"的新接口里(解析器、校验器、客户端调用),等团队熟悉后再逐步替换旧的错误码约定。记住:前沿特性不必全用,能立刻提升代码质量的那几个,才是你的第一桶金。
💡 学习建议:用 std::println 重写一个你熟悉的输出密集小程序,再把你项目里"返回 -1 表示失败"的函数改成 std::expected,体会错误信息随值传递的清爽。建议在本地装一个 GCC 14+,把本文所有示例亲手编译运行一遍。