C++23 是继 C++20 之后的小版本,它没有 C++20 引入 Concepts / ranges / 协程那种「天翻地覆」,而是把一堆高频手写模式收进标准库。所以「要不要升」的答案通常是:值得升,而且风险低——真正的坑不在语言,而在编译器版本。这篇按「开关 → 破坏性改动 → 收益 → 实操」的顺序,给一份能照着做的迁移清单。
1. 引子:先确认编译器跟得上
C++23 的可用性主要由编译器版本决定,而不是「写不写得出新语法」。gcc 到 14、clang 到 17 才算基本完整;更早的版本要么用实验性开关(-std=c++2b),要么干脆缺库。
| 编译器 | 正式开关 | 实验开关 | C++23 基本完整 |
|---|---|---|---|
| GCC | -std=c++23 | -std=c++2b(GCC 11–13) | GCC 14+ |
| Clang | -std=c++23 | -std=c++2b(Clang 16 及以前) | Clang 17+ |
| MSVC | /std:c++latest | — | VS 2022 17.x |
一个真实的坑:gcc 13 在-std=c++23下,__cplusplus宏仍是202100L(GCC 14 才改成202302L)。所以用__cplusplus >= 202302L判断「现在是不是 C++23」在 gcc 13 上会误判。规避办法是用 feature-test 宏(__cpp_lib_*)而不是__cplusplus。下面这段把两者都打印出来对照:
// verify: std=c++23#include<expected>#include<optional>#include<print>intmain(){std::println("__cplusplus = {}",__cplusplus);std::println("__cpp_lib_expected = {}",__cpp_lib_expected);std::println("__cpp_lib_optional = {}",__cpp_lib_optional);}__cplusplus = 202302 __cpp_lib_expected = 202211 __cpp_lib_optional = 202110__cpp_lib_expected = 202211表示「std::expected已可用」,比__cplusplus更精准地对应到「某个具体库特性在不在」。写可移植代码时,判特性用__cpp_lib_*,判语言标准才用__cplusplus。
官方文档:Feature test macros — cppreference:
__cpp_*/__cpp_lib_*的完整对照表,迁移时按需查。
2. 破坏性改动:几乎可以忽略
C++20 → C++23 的破坏性改动非常少,绝大多数现有 C++20 代码原样重编就能过。需要留意的只有几处「废弃 → 将来移除」类的内容:
| 改动 | 类型 | 影响 | 应对 |
|---|---|---|---|
std::aligned_storage | 废弃(deprecated) | 用它的代码在-Wall下会收警告 | 换成alignas+std::byte数组 |
std::aligned_union | 废弃 | 同上 | 换成std::aligned_union的替代或alignas |
个别constexpr能力收紧/修正 | 行为修正 | 极小 | 遇到再看具体报错 |
volatile复合赋值等被废弃 | 废弃 | 老代码可能警告 | 移除不必要的volatile |
结论:语言层面基本无痛,真正的迁移成本在「要不要顺手把老写法换成 C++23 的新写法」,而不是「能不能编过」。
为什么破坏性改动这么少?这是 C++ 标准委员会近年的既定策略:新标准以「新增」为主,删除或改动既有行为要经过漫长的废弃期,尽量不打断现有代码库。所以对已经在 C++20 上稳定运行的项目来说,这次迁移的风险主要来自编译器本身的实现差异(比如 gcc 13 的__cplusplus老行为),而不是语言规则变了导致代码语义改变。
3. 收益清单:哪些值得第一时间替换
升到 C++23 不意味着要重写代码,但下面这几项是「零风险、纯正收益」的替换,值得一升就做。
| C++23 新特性 | 替代谁 | 收益 |
|---|---|---|
std::expected<T,E> | 错误码 / 哨兵值 / 返回pair<bool,T> | 错误显式进类型,不靠约定 |
std::print/std::println | std::cout+ 一堆<< | 类型安全、格式集中、无endl刷缓冲坑 |
monadic optional(and_then/transform/or_else) | 一串if (opt) {...} | 判空链式化,少嵌套 |
std::ranges::to<Container>() | 手写vector(v.begin(), v.end()) | ranges 结果直接落容器 |
替换的优先级原则很简单:先换「零行为差异、纯语法简化」的,再换「行为有细微差别、需要验证」的。std::print和 monadic optional 属于前者:它们只是把原来的写法变短,语义一模一样,替换后跑一遍测试就能确认无差异。std::expected属于后者。它要求你把错误处理从「返回错误码 + 传引用返回结果」改成「返回 expected 对象」,涉及函数签名的改动,要连调用方一起改,适合新模块优先、老模块渐进。
先看std::print的替换:同样的输出,老写法要把格式和变量搅在一串<<里。
// verify: std=c++23#include<print>#include<iostream>#include<string>intmain(){std::string name="喵喵";intscore=95;// C++20 老写法:格式和变量混在 << 里std::cout<<name<<" 考了 "<<score<<" 分\n";// C++23 新写法:一个格式串,变量后置std::println("{} 考了 {} 分",name,score);}喵喵 考了 95 分 喵喵 考了 95 分官方文档:std::print — cppreference:格式串语法与
std::format同源。
再看判空的替换,这是「迁移清单」里收益最直观的一条——一个「字符串转整数再翻倍」的流程,C++20 要写if,C++23 一条链搞定:
// verify: std=c++23#include<optional>#include<string>#include<print>std::optional<int>toInt(conststd::string&s){try{returnstd::stoi(s);}catch(...){returnstd::nullopt;}}intmain(){std::string input="42";// C++20 老写法:if 判空intold=0;autor=toInt(input);if(r.has_value())old=*r*2;// C++23 新写法:monadic 链intnow=toInt(input).transform([](intx){returnx*2;}).value_or(0);std::println("old={} now={}",old,now);}old=84 now=84官方文档:std::optional::and_then — cppreference:
transform/or_else在同一页可以跳到。
4. 迁移实操:分三步走
不建议一次性把整个工程切成 C++23 再改写法。更稳的节奏是「开关先行、兼容过渡、按需替换」。
第 1 步 改编译开关 -std=c++20 -> -std=c++23 │ (CI 里单独开一条 lane 验证) ▼ 第 2 步 兼容期共存 feature-test 宏 + #if 做条件编译, │ 保证 C++20 / C++23 都能编 ▼ 第 3 步 按需替换 先换 expected / print / monadic optional, ranges::to 等顺手了再上第 2 步的关键是:新特性和旧写法可以共存,用 feature-test 宏做闸门,不急着删旧代码。下面这段演示「同一份代码,C++20 和 C++23 都能编」的写法:
// verify: std=c++23#include<optional>#include<string>#include<print>std::optional<int>toInt(conststd::string&s){try{returnstd::stoi(s);}catch(...){returnstd::nullopt;}}intdoubleValue(conststd::string&s){autor=toInt(s);#ifdefined(__cpp_lib_optional)&&__cpp_lib_optional>=202110// C++23 起 optional 才有 monadic 操作,这里用 feature-test 宏做闸门returnr.transform([](intx){returnx*2;}).value_or(0);#else// 回退到 C++20 的手写判空if(r.has_value())return*r*2;return0;#endif}intmain(){std::println("doubleValue(\"21\") = {}",doubleValue("21"));std::println("doubleValue(\"xx\") = {}",doubleValue("xx"));}doubleValue("21") = 42 doubleValue("xx") = 0这样升级不是「一次性切过去」,而是「新代码用新写法、旧代码原地不动、用宏在中间过渡」,风险摊薄到每次小提交里。
5. 延伸阅读
- Feature test macros — cppreference:迁移时查「某个特性哪个版本开始可用」的唯一权威表。
- C++23 — cppreference:C++23 新增特性总览,逐条点进去看细节。
- std::expected — cppreference:迁移清单里收益最大的一项。
- std::print — cppreference:格式串语法与
endl差异的说明。
本知识库内的相关篇目:
- 《C++23 核心新特性:std::expected、std::print、deducing this、monadic optional》 —— C++23 没有 C++11/C++20 那种天翻地覆的变化,但补上了一批「日常高频」的拼图
- 《前瞻:std::generator、reflection 与 C++ 的未来方向》 —— C++23 之后,C++26 正在路上
- 《C++ 跨平台工程:特性检测与 feature-test 宏实战》 —— 一份代码要在 Windows/Linux/macOS 上都能编,关键是不写死平台和标准。顺带把工程与架构这条线也铺一下。
6. 一句话总结
升 C++23 的结论是:开关一改基本就能编过(破坏性改动极少),然后按收益顺序替换:先上std::expected、std::print、monadic optional,用 feature-test 宏做 C++20/C++23 共存的过渡闸门;唯一要记住的坑是 gcc 13 的__cplusplus还是202100L,判特性别用它,用__cpp_lib_*。