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>
int main() {
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>
int main() {
std::string name = "喵喵";
int score = 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(const std::string& s) {
try { return std::stoi(s); } catch (...) { return std::nullopt; }
}
int main() {
std::string input = "42";
// C++20 老写法:if 判空
int old = 0;
auto r = toInt(input);
if (r.has_value()) old = *r * 2;
// C++23 新写法:monadic 链
int now = toInt(input).transform([](int x){ return x * 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(const std::string& s) {
try { return std::stoi(s); } catch (...) { return std::nullopt; }
}
int doubleValue(const std::string& s) {
auto r = toInt(s);
#if defined(__cpp_lib_optional) && __cpp_lib_optional >= 202110
// C++23 起 optional 才有 monadic 操作,这里用 feature-test 宏做闸门
return r.transform([](int x){ return x * 2; }).value_or(0);
#else
// 回退到 C++20 的手写判空
if (r.has_value()) return *r * 2;
return 0;
#endif
}
int main() {
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_*。
970



被折叠的 条评论
为什么被折叠?



