以前只配了一半
很多教程里的 .vscode/launch.json,大概是这个意思:
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug with GDB",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/build/debug/bin/vscode_cpp_debug_demo",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"MIMode": "gdb",
"miDebuggerPath": "/usr/bin/gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
}
]
}
这段不能说错。
VS Code 官方的 C++ 示例里,也是类似这一套结构:type 用 cppdbg,MIMode 用 gdb,setupCommands 里打开 pretty-printing。
但是少了一个关键动作。
你点 F5 的时候,它会去启动 program 指向的那个可执行文件。问题是,这个文件是不是刚刚编出来的?
如果不是,那你调的就是上一次的旧程序。
我以前就经常干这种蠢事。代码改了,断点也打了,调半天发现行为不对,最后才想起来:哦,刚刚没重新编译。
preLaunchTask 才是那个钩子
VS Code 的 Tasks,本质上就是把外部命令挂进编辑器。
编译、测试、打包,原来要在终端敲,现在可以写到 .vscode/tasks.json。然后 launch.json 里的 preLaunchTask,再按 label 去找那一个 task。
一个最小的 CMake 项目,可以这么放:
.
├── .vscode/
│ ├── launch.json
│ └── tasks.json
├── CMakeLists.txt
├── src/
│ └── main.cpp
└── tools/
└── gdb_printers.py
CMakeLists.txt 先写到够用:
cmake_minimum_required(VERSION 3.16)
project(vscode_cpp_debug_demo LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
add_executable(vscode_cpp_debug_demo
src/main.cpp
)
然后是 .vscode/tasks.json:
{
"version": "2.0.0",
"tasks": [
{
"label": "cmake: configure debug",
"type": "process",
"command": "cmake",
"args": [
"-S", "${workspaceFolder}",
"-B", "${workspaceFolder}/build/debug",
"-DCMAKE_BUILD_TYPE=Debug"
],
"problemMatcher": []
},
{
"label": "cmake: build debug",
"type": "process",
"command": "cmake",
"args": [
"--build", "${workspaceFolder}/build/debug",
"--parallel"
],
"dependsOn": ["cmake: configure debug"],
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": ["$gcc"]
}
]
}
注意这里的 cmake --build。
CMake 官方给的构建入口就是这个形式:cmake --build <dir>。它会去调用背后的原生构建工具,比如 Make、Ninja、MSBuild 这一类。
也就是说,VS Code 不需要知道你的项目到底该敲 make 还是 ninja。
这件事交给 CMake。
F5 之前,先编译
然后回到 .vscode/launch.json,把 preLaunchTask 补进去:
{
"version": "0.2.0",
"configurations": [
{
"name": "CMake Debug with GDB",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/build/debug/bin/vscode_cpp_debug_demo",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"miDebuggerPath": "/usr/bin/gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
],
"preLaunchTask": "cmake: build debug"
}
]
}
这一行才是重点:
"preLaunchTask": "cmake: build debug"
按 F5 的链路就变成了:
launch.json
-> preLaunchTask
-> tasks.json: cmake: build debug
-> dependsOn: cmake: configure debug
-> cmake -S ... -B ...
-> cmake --build ...
-> gdb 启动最新的 program
如果是 Windows + MinGW,program 大概率要改成带 .exe 的路径,miDebuggerPath 也要指到自己的 gdb.exe。
如果是 Visual Studio Generator 那种多配置生成器,构建参数通常还要加 --config Debug。
但是主线不变。
调试前让 VS Code 先跑一次构建任务,别靠自己脑子记。
还有一个痛点:自定义类型看不懂
只把编译链路打通,还不够。
C++ 代码里稍微封装一点业务类型,VS Code 左边那个变量窗口就开始变得难看。
比如下面这个例子。
src/main.cpp:
#include <cstdint>
#include <cstdio>
#include <string_view>
#include <vector>
namespace market {
struct Price {
std::int64_t raw{};
static Price from_double(double value) {
return Price{static_cast<std::int64_t>(value * 10000)};
}
double to_double() const {
return static_cast<double>(raw) / 10000.0;
}
};
struct Instrument {
char symbol[16]{};
Price last;
};
Instrument make_instrument(std::string_view symbol, double price) {
Instrument instrument;
std::snprintf(instrument.symbol, sizeof(instrument.symbol), "%s", symbol.data());
instrument.last = Price::from_double(price);
return instrument;
}
} // namespace market
int main() {
std::vector<market::Instrument> watchlist{
market::make_instrument("IF2406", 3578.6),
market::make_instrument("IH2406", 2468.2),
};
market::Price limit = market::Price::from_double(3600.5);
// 在这里打断点,观察 watchlist 和 limit。
return watchlist.empty() || limit.raw == 0;
}
market::Price 在业务里是价格。
但是调试器默认看到的可能只有:
limit = {raw = 36005000}
没错,数据是对的。
但是我脑子里还要自己把 36005000 换算成 3600.5000。如果项目里有一堆 Price、Quantity、OrderId、Instrument、AlgoState,调试窗口很快就变成结构体坟场。
这个时候就该上 GDB pretty-printer 了。
GDB 官方手册说得很直接:它提供了一套机制,可以用 Python 代码 pretty-print 值,而且这个机制同时适用于 MI 和命令行。
VS Code 的 C++ 调试刚好走的就是 GDB/MI 这条路。
给业务类型写一个 Python 展示层
新建 tools/gdb_printers.py:
import gdb
import gdb.printing
class PricePrinter:
def __init__(self, val):
self.val = val
def to_string(self):
raw = int(self.val["raw"])
return f"{raw / 10000.0:.4f}"
class InstrumentPrinter:
def __init__(self, val):
self.val = val
def to_string(self):
symbol = self.val["symbol"].string()
price_raw = int(self.val["last"]["raw"])
price = price_raw / 10000.0
return f"{symbol} last={price:.4f}"
def build_pretty_printer():
printer = gdb.printing.RegexpCollectionPrettyPrinter("market")
printer.add_printer("Price", "^market::Price$", PricePrinter)
printer.add_printer("Instrument", "^market::Instrument$", InstrumentPrinter)
return printer
gdb.printing.register_pretty_printer(
gdb.current_objfile(),
build_pretty_printer(),
replace=True,
)
然后在 launch.json 的 setupCommands 里把它 source 进去:
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
},
{
"description": "Load project pretty-printers",
"text": "-interpreter-exec console \"source ${workspaceFolder}/tools/gdb_printers.py\"",
"ignoreFailures": false
}
]
重新 F5。
这时候再看调试窗口,Price 的展示就应该接近:
limit = 3600.5000
Instrument 也会从一坨数组、结构体,变成更接近业务的:
IF2406 last=3578.6000
这里我故意说“接近”,因为 pretty-printer 要和编译器、类型名字、真实字段布局匹配。
如果你的业务类型里面又套了 std::vector、std::array、智能指针,Python 脚本里访问的字段可能还要继续调。不要指望一个脚本通吃所有项目。
但是思路是稳的。
把“调试时人想看的样子”,单独写成一层 Python 展示逻辑。
别把 launch.json 当成全部
现在再看这套配置,我觉得至少有三层。
.vscode/launch.json 负责“怎么启动调试器”。
.vscode/tasks.json 负责“启动调试器之前,先把项目编出来”。
tools/gdb_printers.py 负责“断住以后,变量用人能看懂的方式展示”。
以前我只配第一层。
能用,但是很毛坯。
真正顺手的 C++ 调试,不应该是“我先去终端编译一下,再回来按 F5,再在变量窗口里心算字段含义”。
更舒服的链路应该是:按 F5,VS Code 自动跑 CMake,GDB 自动加载项目的 Python printer,断点停下来以后,调试窗口直接说业务语言。
AI 这次帮我配项目,最有价值的地方倒不是生成了多少 C++ 代码。
而是它把这些老工具之间的胶水也补上了。
我以前不是不知道 CMake,也不是不知道 GDB。
只是少配了中间那几个钩子。
参考资料
- Using C++ on Linux in VS Code
- VS Code: Integrate with External Tools via Tasks
- CMake cmake(1) manual
- GDB Manual: Pretty Printing
- GDB Manual: Writing a Pretty-Printer
写作附记
原始提示词
提示词:$blog-writer vscode 进行 C++ 开发,以前查看的教程,都只是简单的配置了 launch.json 里面的 gdb 调试信息,都没提到配置 preLaunchTask,能做到自动触发 cmake 编译,这还是 AI 配置新项目发现的好东西;如果代码有很多自定义的数据类型,vscode 调试窗口无法直接展示,可以引入 python 脚本进行预处理,这样都能展示了。针对上面的内容,都需要提供实际的代码案例。
写作思路摘要
- 主线放在“以前只配 launch.json,其实少了调试前构建和调试时展示两层胶水”。
- 保留用户给出的触发点:AI 配新项目时发现
preLaunchTask可以自动触发 CMake 编译。 - 代码案例按一个最小 CMake 项目组织,包含
CMakeLists.txt、tasks.json、launch.json、C++ 自定义类型和 GDB Python pretty-printer。 - 事实核验优先看 VS Code、CMake、GDB 官方文档;正文不翻译文档,只保留和这条调试链路有关的事实。
- 对 pretty-printer 的边界做了提醒:类型名、标准库实现、字段布局会影响 Python 脚本,项目里要按自己的类型改。