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++の日常的なデバッグにおける非常に面倒な2つの空白部分――起動前のビルドブレークポイント後の変数表示――を完璧に埋めてくれたのです。

以前只配了一半

多くのチュートリアルにある .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++ サンプルでも、同様の構造が使われています:typecppdbg を使い、MIModegdb を使い、setupCommands で pretty-printing を有効にしています。 しかし、決定的に欠けている動作があります。 F5 を押すと、それは program が指す実行ファイルを起動します。問題は、そのファイルが今まさにコンパイルされたものかどうかです? もしそうでなければ、前回ビルドした古いプログラムをデバッグしてしまいます。 私も以前、よくこんな馬鹿なことをしていました。コードを変更し、ブレークポイントも設定したのに、しばらく動かしてみたら動作がおかしいことに気づき、最後に「ああ、さっき再コンパイルするのを忘れていた」と思い出す、ということがありました。

preLaunchTask こそがそのフックである

VS Code の Tasks は、本質的に外部コマンドをエディタに組み込む仕組みです。 コンパイル、テスト、パッケージングといった作業は、以前はターミナルで手動で行う必要がありましたが、今では .vscode/tasks.json に記述できます。そして launch.json の中の preLaunchTask が、そのラベルを使って該当するタスクを探し出します。 最小限の 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(Machine Interface)とコマンドラインの両方に適用できるということです。 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 にこれをソースとして読み込ませます:

"setupCommands": [
  {
    "description": "gdbのプリティプリンティングを有効化",
    "text": "-enable-pretty-printing",
    "ignoreFailures": true
  },
  {
    "description": "プロジェクトのプリティプリンターをロード",
    "text": "-interpreter-exec console \"source ${workspaceFolder}/tools/gdb_printers.py\"",
    "ignoreFailures": false
  }
]

F5 を再実行します。 この後、デバッグウィンドウを見ると、Price の表示は以下に近くなるはずです:

limit = 3600.5000

Instrument もまた、単なる配列や構造体から、よりビジネスに近いものになります:

IF2406 last=3578.6000

ここで「近い」と言っているのは意図的です。なぜなら、プリティプリンターはコンパイラ、型名、実際のフィールドレイアウトと一致させる必要があるからです。 もしあなたのビジネスタイプの中に std::vectorstd::array、スマートポインタなどがネストされている場合、Pythonスクリプトからアクセスするフィールドはさらに深い呼び出しが必要になるかもしれません。一つのスクリプトですべてをカバーできると期待しないでください。 しかし、考え方はしっかりしています。 「デバッグ時に人間が見たい姿」を、独立したPythonの表示ロジックとして記述するのです。

launch.json を全てだと思ってはいけない

この設定群を見ると、少なくとも3層あると感じます。 .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 が新しいプロジェクトを設定する上で非常に役立つものです。もしコードにカスタムデータ型が多く含まれていて、VS Code のデバッグウィンドウで直接表示できない場合でも、Python スクリプトを導入して前処理を行うことで、それらも表示できるようになります。上記の内容について、すべて実際のコード例を提供してください。

ライティングの骨子まとめ

  • メインテーマを「これまで launch.json の設定だけだったが、実際にはデバッグ前のビルドとデバッグ時の表示という2層の接着剤(仕組み)が欠けていた」ことに置く。
  • ユーザーから提供されたトリガーポイントは維持する:AI が新しいプロジェクトを設定する際に、preLaunchTask で CMake のコンパイルが自動的にトリガーされることを発見した点。
  • コード例は最小限の CMake プロジェクトで構成し、CMakeLists.txttasks.jsonlaunch.json、C++ のカスタム型、および GDB Python pretty-printer を含める。
  • 事実確認は VS Code、CMake、GDB の公式ドキュメントを最優先とし、本文ではドキュメント全体を翻訳せず、このデバッグフローに関連する事実のみを残す。
  • pretty-printer に関しては、型名、標準ライブラリの実装、フィールドレイアウトなどが Python スクリプトに影響を与える点について注意喚起を行う。プロジェクト内では自身の型に合わせて修正が必要である旨を明記する。
金融ITプログラマーのいじくり回しと日常のつぶやき
Hugo で構築されています。
テーマ StackJimmy によって設計されています。