|
|
这两年在老项目里逐步接入 C++17/20,我的体感是“别想着一口吃成个胖子”,而是把它当成一次长期的代码健康管理。老代码不是天然敌人,但它对编译器、ABI、第三方库版本往往耦合得很深。贸然全量开启 -std=c++20,多半是一地鸡毛。更稳妥的策略是从编译器链路、语言特性、库特性、工具链三条线分步推进,每一步都能独立落地、可回滚。
第一步通常是“编译器与CI先行”。把构建脚本里标准开关分模块调整:核心库先维持 c++14,外围工具、测试子目录试点 c++17/20。CI 里并行两路构建,老标准做回归,新能源做冒烟,保证任何破坏性变化都能在小范围内暴露。同时尽早升级静态分析器和格式化工具(clang-tidy/clang-format),用现代规则集扫描潜在雷点,比如异常规范、三方依赖里对 std::result_of 的使用在 C++20 下已废弃。
语言特性我更建议“从不破坏 ABI 的糖开始”。C++17 的结构化绑定、if/switch 初始化、[[nodiscard]]、std: ptional/variant/string_view 基本零成本,却能立刻提升可读性和意图表达。我的做法是:公共头文件里只增加向后兼容的声明(例如用 using alias 暴露 std: ptional),实现文件里大胆用结构化绑定和 if 带初始化,把原有的临时变量与“哑标志”收敛掉。注意 string_view 的生命周期陷阱:避免从临时 std::string 构造后跨语句存活,必要时在接口层仍以 string 接收,内部再转 view。
到了并发与内存模型,C++20 的 std::jthread、stop_token、latch/barrier 能有效替换部分自研轮子。我的迁移顺序是先在测试/工具进程里用 jthread 验证取消语义,再把部分后台任务从 thread+atomic 的取消标志迁到 stop_token,记录清理点与不可中断区间。生产路径里要关注第三方库是否与新的线程原语和平共处,特别是与老式线程池混用时的线程亲和和销毁时序。
模板与元编程方面,concepts 很香,但对编译时与错误信息的影响不容忽视。我的经验是:先把常用的 enable_if/void_t 约束,用最小集合的命名 Concepts 封装(比如 RangeLike、Hashable);仅在新公共模板接口上启用概念,旧接口内部使用 requires 约束保证实现分支更清晰。这样做的副作用是编译时间略增,但团队调试体验提升明显。consteval/constexpr 扩展可以逐步把配置表和状态机的部分查表逻辑前移到编译期,前提是别把编译时间炸到开发者无法忍受;CI 里监控编译耗时趋势是必要的。
库层面,std::filesystem 是最容易取得收益的一块。我的套路是先写一层薄适配,把历史上的路径工具函数转发到 filesystem,统一路径分隔与编码,再逐文件替换低风险调用。这样既能保留旧接口签名,也能在新代码里用强类型路径,减少手工拼接 bug。类似地,std::chrono 在 C++20 的日历/时区库也值得引入,但要留意平台 tzdb 的可用性和更新机制,容器化环境里最好随镜像打包 tzdata。
工具与风险控制是这场渐进式改造的安全网。引入工具型依赖(fmt、gsl、expected 等)时,先把它们当作第三方库,以适配层暴露到代码里;等团队使用习惯稳定,再逐步切换到标准等价物(如 std::format、std::expected)。这样即使编译器版本滞后,也不至于卡死在某个特性上。对于可能引入 UB 行为变化的地方(如 constexpr 下的未定义访问、迭代器失效检查),加上编译期断言和运行期卫兵,配合 sanitizers 做夜间构建。
最后,沟通和节奏比技术细节更关键。给团队定一个 |
|