ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

C++20 升 C++23 迁移清单:开关、破坏性改动与收益

C++20 升 C++23 迁移清单:开关、破坏性改动与收益

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::printlnstd::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_*。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进