VS Code 调 C++,别漏了 CMake 和 GDB Printer

以前我在 VS Code 里调 C++,配置基本就停在 launch.json,最多再加一句 GDB。

program 填好,把 gdb 填好,把断点打好。然后呢?然后每次调试前,自己去终端里 cmake --build 一下。

更烦的是,自定义的价格、合约、订单类型断下来以后,VS Code 调试窗口经常只能看到一堆内部字段。数据是对的,但是不说人话。

这事离谱的地方在于,我以前看过的一些教程也差不多就写到 launch.json 为止。直到最近让 AI 配一个新项目,它顺手加了 preLaunchTaskgdb_printers.py,我才反应过来:VS Code 调 C++,不只是把 GDB 启起来。调试前可以自动触发 CMake 编译,断点停住以后,也可以让 GDB 加载 Python 脚本,把业务类型处理成自己看得懂的样子。

说实话,这不是什么黑科技。

但是它刚好补上了 C++ 日常调试里两块很烦的空白:启动前构建,以及断点后的变量展示

以前只配了一半

很多教程里的 .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++ 示例里,也是类似这一套结构:typecppdbgMIModegdbsetupCommands 里打开 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。如果项目里有一堆 PriceQuantityOrderIdInstrumentAlgoState,调试窗口很快就变成结构体坟场。

这个时候就该上 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.jsonsetupCommands 里把它 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::vectorstd::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。

只是少配了中间那几个钩子。

参考资料

写作附记

原始提示词

提示词:$blog-writer vscode 进行 C++ 开发,以前查看的教程,都只是简单的配置了 launch.json 里面的 gdb 调试信息,都没提到配置 preLaunchTask,能做到自动触发 cmake 编译,这还是 AI 配置新项目发现的好东西;如果代码有很多自定义的数据类型,vscode 调试窗口无法直接展示,可以引入 python 脚本进行预处理,这样都能展示了。针对上面的内容,都需要提供实际的代码案例。

写作思路摘要

  • 主线放在“以前只配 launch.json,其实少了调试前构建和调试时展示两层胶水”。
  • 保留用户给出的触发点:AI 配新项目时发现 preLaunchTask 可以自动触发 CMake 编译。
  • 代码案例按一个最小 CMake 项目组织,包含 CMakeLists.txttasks.jsonlaunch.json、C++ 自定义类型和 GDB Python pretty-printer。
  • 事实核验优先看 VS Code、CMake、GDB 官方文档;正文不翻译文档,只保留和这条调试链路有关的事实。
  • 对 pretty-printer 的边界做了提醒:类型名、标准库实现、字段布局会影响 Python 脚本,项目里要按自己的类型改。
金融IT程序员的瞎折腾、日常生活的碎碎念
使用 Hugo 构建
主题 StackJimmy 设计