Only Half Was Configured Before
The .vscode/launch.json in many tutorials is probably something like this:
{
"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
}
]
}
]
}
This part cannot be wrong.
The official C++ example in VS Code also uses a similar structure: type uses cppdbg, MIMode uses gdb, and pretty-printing is enabled in setupCommands.
But one crucial step is missing.
When you press F5, it launches the executable file pointed to by program. The problem is, was this file just compiled?
If not, then you are debugging the old program from last time.
I used to do this stupid thing often. I changed the code and set breakpoints, debugged for a while, only to realize later: Oh, I forgot to recompile.
preLaunchTask is the hook
VS Code Tasks are essentially about hooking external commands into the editor.
Compiling, testing, and packaging—what used to be typed in the terminal can now be written into .vscode/tasks.json. Then, preLaunchTask in launch.json finds that task by its label.
For a minimal CMake project, it could look like this:
.
├── .vscode/
│ ├── launch.json
│ └── tasks.json
├── CMakeLists.txt
├── src/
│ └── main.cpp
└── tools/
└── gdb_printers.py
Here is the CMakeLists.txt first:
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
)
And here is .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"]
}
]
}
Notice the cmake --build here.
The official CMake build entry point is this format: cmake --build <dir>. It calls the underlying native build tool, such as Make, Ninja, or MSBuild.
This means VS Code doesn’t need to know whether your project should use make or ninja.
CMake handles that part.
Before F5, first compile
Then go back to .vscode/launch.json and add 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"
}
]
}
This line is the key:
"preLaunchTask": "cmake: build debug"
The flow when pressing F5 becomes:
launch.json
-> preLaunchTask
-> tasks.json: cmake: build debug
-> dependsOn: cmake: configure debug
-> cmake -S ... -B ...
-> cmake --build ...
-> gdb starts the latest program
If you are on Windows + MinGW, the program probably needs to be changed to a path with .exe, and miDebuggerPath should also point to your own gdb.exe.
If it’s a multi-configuration generator like Visual Studio Generator, you usually need to add --config Debug to the build arguments.
But the main flow remains the same.
Before debugging, let VS Code run the build task first; don’t rely on memory alone.
Another Pain Point: Incomprehensible Custom Types
Just connecting the compilation link is not enough.
If you wrap a business type slightly in your C++ code, the variable window on the left side of VS Code starts looking ugly.
Take this example.
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);
// Set a breakpoint here to observe watchlist and limit.
return watchlist.empty() || limit.raw == 0;
}
In the business logic, market::Price represents a price.
But what the debugger might default to showing is only:
limit = {raw = 36005000}
Yes, the data is correct.
But in my head, I still have to manually convert 36005000 back to 3600.5000. If a project has a bunch of Price, Quantity, OrderId, Instrument, AlgoState, the debug window quickly turns into a structure graveyard.
This is when you need GDB pretty-printers.
The official GDB manual states it very clearly: It provides a mechanism that can use Python code to pretty-print values, and this mechanism works for both MI and command line.
VS Code’s C++ debugging uses exactly this GDB/MI path.
Writing a Python Presentation Layer for Business Types
Create 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,
)
Then source it in launch.json’s setupCommands:
"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
}
]
Press F5 again.
When you look at the debug window this time, the display for Price should be close to:
limit = 3600.5000
And Instrument will change from a blob of arrays and structs to something closer to the business logic:
IF2406 last=3578.6000
I deliberately say “close to” here because the pretty-printer needs to match the compiler, type names, and actual field layout.
If your business types contain nested std::vector, std::array, or smart pointers, you might need to write more code in Python to access those fields. Don’t expect one script to cover everything.
But the idea is solid.
Separate what “you want to see when debugging” into its own layer of Python presentation logic.
Don’t treat launch.json as everything
Looking at this set of configurations now, I think there are at least three layers.
.vscode/launch.json is responsible for “how to start the debugger.”
.vscode/tasks.json is responsible for “building the project first before starting the debugger.”
tools/gdb_printers.py is responsible for “displaying variables in a human-readable way after a breakpoint.”
Before, I only configured the first layer.
It worked, but it was very rudimentary.
Smooth C++ debugging shouldn’t be like: “I have to compile it in the terminal first, then come back and press F5, and then manually calculate the meaning of fields in the variables window.”
The more comfortable workflow should be: Press F5, VS Code automatically runs CMake, GDB automatically loads the project’s Python printer, and when a breakpoint is hit, the debug window directly speaks business language.
What was most valuable about AI helping me set up this project wasn’t how much C++ code it generated.
It was that it bridged these old tools together.
It’s not that I didn’t know CMake before, or that I didn’t know GDB.
I just missed configuring those intermediate hooks.
References
- 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
Writing Notes
Original Prompt
Prompt: $blog-writer vscode for C++ development, previous tutorials only showed simple configuration of gdb debugging information in launch.json, and didn’t mention configuring preLaunchTask. Being able to automatically trigger cmake compilation is a great feature when setting up new projects with AI; if the code has many custom data types that cannot be directly displayed in the VS Code debug window, one can introduce a Python script for preprocessing so that they can all be displayed. Regarding the above content, actual code examples are needed for everything.
Writing Idea Summary
- The main thread should focus on the idea that “previously, only
launch.jsonwas configured, but it missed the two layers of glue: pre-build and runtime display during debugging.” - Keep the trigger point provided by the user: Discovering that when setting up a new AI project,
preLaunchTaskcan automatically trigger CMake compilation. - Organize code examples around a minimal CMake project, including
CMakeLists.txt,tasks.json,launch.json, custom C++ types, and GDB Python pretty-printer. - For factual verification, prioritize official documentation from VS Code, CMake, and GDB; the main text should not translate entire documents but only retain facts relevant to this debugging pipeline.
- Provide a warning regarding the boundaries of the pretty-printer: Type names, standard library implementations, and field layouts can affect the Python script, so users must adapt it for their specific types in the project.