C++ 技术栈:从编译器到上线
很多人一提到 C++ 技术栈,想到的是「一个编译器加一个 IDE」。但真正在项目里干活时,编译器只占很小一环:代码怎么写进去、错误怎么查出来、依赖怎么锁住、性能问题怎么定位,这些才决定了你每天是愉快还是痛苦。
这篇文章把我日常在用的那一套整理出来。原则只有两条:换一台机器也能一条命令构建,出问题能在十分钟内定位。
一、编译器:选一个,但要知道另外两个的脾气
主流的就三家:GCC、Clang、MSVC。我的默认是 Clang,原因是诊断信息可读性好、对 sanitizer 支持最全;GCC 在 Linux 服务器上生态最稳,很多发行版默认;MSVC 则是 Windows 上唯一的选择,而且它对标准库和 ODR 的严格程度经常能提前暴露跨平台问题。
语言标准按项目明确写死,别指望默认值:
-std=c++20 # 或 c++23;显式写,别用 -std=gnu++17 这种隐式扩张
-Wall -Wextra -Wpedantic
-Wconversion -Wshadow # 团队能忍就加,能省掉很多隐式转换 bug
如果项目要跨平台,CI 上至少跑两个编译器。只在 Clang 上通过的代码,换 GCC 常常会报「某个名字在某个模板实例化点不可见」——这不是编译器挑剔,是代码确实不规范。
二、构建:CMake + Ninja,别再手写编译器命令
CMake 有它的历史包袱,但它是事实标准,第三方库的集成成本最低。关键是用 target-based 写法,把编译器选项、include 目录、宏都挂在 target 上,而不是全局 include_directories():
cmake_minimum_required(VERSION 3.25)
project(hello LANGUAGES CXX)
add_executable(hello src/main.cpp)
target_compile_features(hello PRIVATE cxx_std_20)
target_compile_options(hello PRIVATE -Wall -Wextra -Wpedantic)
# 依赖只跟着这个 target 走,别的 target 不会被动继承
find_package(fmt REQUIRED)
target_link_libraries(hello PRIVATE fmt::fmt)
生成器用 Ninja,增量构建比 Make 快一个档次。再配 CMakePresets.json,本地和 CI 用同一份配置,就不会出现「我这儿能编」的经典事故。重复构建多的话加 ccache,改一个文件后的重编时间能从十几秒降到一秒以内。
三、依赖管理:三条路线,按项目规模挑
- FetchContent:依赖少、版本要求不复杂的小项目。零外部工具,CMake 原生,缺点是没有二进制缓存,全量构建会重新编译依赖。
- vcpkg / Conan 2:依赖多、跨平台、或者 CI 需要缓存时。它们解决的核心问题不是「下载」,而是「同一份依赖在不同机器上编出一致的结果」。
- 系统包 + pkg-config:给 Linux 发行版打包时最省心。缺点是版本受发行版约束,别人装不到同样的版本。
我的习惯是内部小库走 FetchContent,对外发布的产品用 vcpkg manifest 模式,把依赖版本全部写进 vcpkg.json 并提交到仓库。
四、调试与诊断:先让程序自己招供
遇到崩溃,第一反应不该是加 printf,而是让 sanitizer 说话。Debug 构建和 CI 里常开:
-fsanitize=address,undefined:越界、释放后使用、未定义行为,一次跑测试就能抓到一堆。-fsanitize=thread:数据竞争。注意它和 ASan 不能同时开,通常单独跑一轮。-fno-omit-frame-pointer:保证perf之类的采样工具能拿到完整调用栈,发布构建也建议保留。
交互式调试用 lldb 或 gdb。把 ptype、frame variable 和条件断点用熟,比在任何 IDE 里点来点去都快。挂死在线上又没法重编时,valgrind 的 memcheck 依然是最省事的兜底——代价是慢 20 倍以上。
五、性能:先测量,再优化
没有测量就没有优化。工作流一般是:perf record 采一段时间 → perf report 看热点 → 只改那一个函数 → 用基准测试确认真的变快了。
微基准用 Google Benchmark,别自己拿 chrono 手搓循环——编译器会把你的循环优化掉,或者因为缓存预热给出过于乐观的数字。想确认某段代码到底生成了什么指令,扔进 Compiler Explorer 看汇编,比对着源码猜有效得多。
发布构建用 -O2。别用 -march=native:它会让二进制只在你自己的 CPU 上跑得快,在别人的机器上直接 Illegal instruction。
六、代码质量:让机器管格式,让人管设计
clang-format 定格式,clang-tidy 定规则,两者都接进 pre-commit。格式争论是纯浪费,把它交给工具之后,代码评审就只剩下真正值得讨论的东西:接口是否合理、错误怎么传播、所有权归谁。
七、我的一份最小可用组合
不想纠结的话,直接抄这一份:
- 编译器:本地 Clang,CI 加跑 GCC
- 构建:CMake + Ninja + ccache,配置进 CMakePresets
- 依赖:小项目 FetchContent,产品 vcpkg manifest
- 调试:lldb + ASan/UBSan(Debug)、valgrind(兜底)
- 性能:perf + Google Benchmark + Compiler Explorer
- 规范:clang-format + clang-tidy + pre-commit
这套东西没有一样是新奇的,全部是过去十年里反复被验证过的默认选项。技术栈的价值不在于「全」或者「潮」,而在于稳定到你可以不去想它——需要的时候它在,不需要的时候它不打扰你。