Visual Studio 2019でC++の大規模ソリューションをデバッグしていると、「ビルドは通っているのにDLLが更新されない」「ブレークポイントが“ソースコードが元のバージョンと異なります”で無効になる」といった現象に悩まされることがあります。本記事では、staticライブラリ(.lib)を多数のDLLが共有する構成で起きやすい原因と、確実に直す設定・切り分け手順をまとめます。
現象:コード変更がDLLに反映されず、ブレークポイントが無効になる
まず、よくある状況を整理します。C++の大規模ソリューション(共通のstaticライブラリ+それをリンクする多数のDLL+ホストアプリexe)では、ほんの小さな変更でもビルド・リンク・デバッグの流れが複雑になり、更新漏れが起きると原因が見えにくくなります。
| 項目 | 内容(例) | ポイント |
|---|---|---|
| 開発環境 | Visual Studio 2019 Professional 16.7.7 | 設定差が挙動に直結しやすい |
| 構成 | 共通staticライブラリ(.lib)+多数のDLL+ホストexe | 依存関係の向き(誰が誰に依存するか)が重要 |
| 症状 | 変更した.cppのブレークポイントが無効化される | デバッガが参照しているDLL/PDBが古い可能性 |
| 表示メッセージ | 「ソースコードが元のバージョンと異なります」 | ソースと、読み込まれたモジュールのデバッグ情報が食い違っている |
この手の問題は、単純に「コンパイルされていない」だけでなく、ビルドはされているが、実行時に参照されるDLLが更新されていない/別の場所のDLLが読み込まれている/PDBが一致していないといった複数のパターンで発生します。
再現しやすい流れと、起きていること
典型的には次のような流れで遭遇します。
- デバッグ中のホストアプリ(exe)を
Shift + F5で停止 - 共通staticライブラリ側のソース(.cpp)を少し修正
F7でビルド(staticライブラリは再ビルドされ、出力ログ上はDLLもリンクされたように見える)F5でデバッグ開始- 変更箇所に置いたブレークポイントが「ソースコードが元のバージョンと異なります」と表示され無効化
状況をよく観察すると、次のような“矛盾”が見つかります。
- 該当.cppより.objのタイムスタンプが新しい(= コンパイル自体は動いている)
- しかしDLLのタイムスタンプが.cpp/.objより古いことがある(= DLLが古いまま)
- DLLを削除してからビルドすると、DLLが再生成されブレークポイントが正常に動く
- まれにF5/F7の組み合わせでうまくいくこともあり、「ときどき直る」挙動に見える
ここで重要なのは、ブレークポイントが無効になるのは「Visual Studioが意地悪をしている」わけではなく、デバッガがロードしたDLLと、あなたが開いているソースコードが対応していないという合理的な理由がある、という点です。
ブレークポイントが無効になる代表パターン
メッセージが似ていても原因は複数あります。まずは代表例を一覧にしておくと、切り分けが速くなります。
| 見え方 | よくある原因 | 最短の確認ポイント |
|---|---|---|
| 「ソースコードが元のバージョンと異なります」 | ロードしたDLL/PDBが古い、または別パスのDLLを実行している | デバッグ中に「モジュール」ウィンドウでDLLのパスとPDB一致を確認 |
| 「シンボルが読み込まれていません」 | PDBが見つからない/シンボル設定が違う | モジュールで「シンボルの状態」を確認し手動読み込み |
| ブレークポイントは有効だが止まらない | 最適化(Release相当)やインライン化、別分岐を通っている | Debug構成か、最適化設定、インライン展開を疑う |
| 削除・クリーンで直る | 出力物の更新漏れ、コピー先の古いDLLを読んでいる | ビルド出力先と実行時参照先を突き合わせる |
今回のテーマは、この中でも特に多い「ロードしたDLL/PDBが古い(更新されていない)」系の原因です。
根本原因:F5時に「スタートアップ+依存関係だけをビルド」が有効だった
結論から言うと、Visual Studioのオプションにある次の設定が原因になるケースがあります。
「実行時にスタートアップ プロジェクトと依存関係のプロジェクトのみをビルドする(Only build startup projects and dependencies on Run)」
そして、ここにもう1つ条件が重なるとハマりやすくなります。
- スタートアッププロジェクトを、staticライブラリ(.lib)側のプロジェクトにしている
- または、DLL/ライブラリをスタートアップにして「デバッグで起動する外部プログラム(ホストexe)」を設定している(プラグイン開発でよくある)
この状態でF5(デバッグ開始)を押すと、Visual Studioの挙動は次のロジックに寄ります。
- F5は「実行の前に必要なビルドをする」
- ただし上記オプションがONだと、ビルド対象は「スタートアッププロジェクト+その依存関係」だけに限定される
- 依存関係とは、あくまで“スタートアップが必要とする側”(= スタートアップが依存しているプロジェクト)
ここが落とし穴です。共通staticライブラリ(.lib)をスタートアップにすると、構造上こうなります。
(依存の向き) DLLプロジェクト → 共通staticライブラリ(.lib) (F5でビルドされる範囲:スタートアップ+依存関係) 共通staticライブラリ(.lib) → (依存しているものがあればそれ)
つまり、DLLは「ライブラリに依存している側(= dependent)」であり、「ライブラリが依存している側(= dependency)」ではありません。だからF5の対象にならず、DLLのリンクが走らない(古いDLLが残る)ことがあります。
結果として、
- .cpp → .obj は新しい(ライブラリはビルドされる)
- しかしDLLは再リンクされず古いまま(ホストexeがロードするDLLは古い)
- デバッガは古いDLL/PDBに基づいてソースを照合する
- 開いているソースは変更後なので一致せず、ブレークポイントが無効になる
| 設定 | F5時のビルド範囲 | この構成(lib→DLL多数)への相性 |
|---|---|---|
| ON(スタートアップ+依存関係のみ) | 限定的(スタートアップが依存しているものだけ) | スタートアップがlibだとDLLは対象外になりやすく、更新漏れの温床 |
| OFF | ソリューション構成に従って、関連プロジェクトがビルドされやすい | 更新漏れを避けやすい(大規模構成ほど効果が出る) |
解決手順:オプションをOFFにする
最も確実で再現性が高いのは、該当オプションをOFFにすることです。
- Visual Studioのメニューから [ツール] → [オプション] を開く
- [プロジェクトおよびソリューション] → [ビルドおよび実行] を開く
- 「実行時にスタートアップ プロジェクトと依存関係のプロジェクトのみをビルドする(Only build startup projects and dependencies on Run)」 のチェックをオフにする
- 必要なら一度ソリューションをビルドし直し(
Ctrl + Shift + Bなど)、F5で再デバッグ
これにより、F5でデバッグを開始する際に、ソリューションの構成に従って関連プロジェクトがビルド・リンクされやすくなり、staticライブラリの変更がDLLへ確実に反映されます。
別解:スタートアッププロジェクトを「ホストexe」に戻すと安定する
設定OFFがシンプルな解決策ですが、運用面では「スタートアップはホストexe」にするのが分かりやすく、チームでも事故が減ります。
- ホストexeをスタートアッププロジェクトにする
- DLLやstaticライブラリはホストexeの依存関係として自然にビルドされる(依存の向きが正しい)
- F5 = 「ホスト起動&必要なものが全部最新」になり、デバッグ体験が安定
どうしてもDLL/ライブラリ側プロジェクトからホストexeを起動したい場合は、少なくともstaticライブラリをスタートアップにするより、ホストexeを起動するDLL側プロジェクトをスタートアップにしたほうが、依存関係の流れに沿いやすくなります。
なぜ「他プロセスがDLLを掴んでいるならリンクエラーになるはず」なのに起きるのか
ここが混乱ポイントになりがちです。もし本当にDLLファイルがロックされていてリンクが出力できないなら、多くの場合はリンク時にエラー(例:ファイルを開けない系)が出ます。
しかし今回のパターンでは、そもそもDLLのリンクが実行されていない(ビルド対象になっていない)ため、リンクエラーも出ません。古いDLLがそのまま残り、ホストexeがそれをロードしてしまいます。
「コンパイル・リンクメッセージが出ているのに…」と感じる場合、次のどれかが混ざっていることもあります。
| “リンクされたように見える”理由 | 実際に起きていること | 確認ポイント |
|---|---|---|
| ライブラリ側の出力ログだけ見ている | libは作られたが、DLLプロジェクトはビルド対象外で何もしていない | 出力ウィンドウでDLLプロジェクト名の「リンク」行があるか |
| 別構成(x86/x64、Debug/Release)を見ている | ビルドしたDLLと、実行時にロードされるDLLが別 | 実行中モジュールの「実際のパス」を確認 |
| 出力先が複数あり、古いDLLが残っている | ビルドはAに出ているが、exeはBからDLLをロードしている | ホストexeの作業ディレクトリ、PATH、プラグイン配置先を確認 |
| インクリメンタルな“更新不要判定”が働いた | 依存関係の定義不足で「DLLは最新」と誤判定 | プロジェクトの依存関係/ビルド順序、出力物の更新日時を確認 |
まずはここを見る:デバッグ中に「読み込まれたDLL」と「PDB」を一致させる
ブレークポイントが無効になったとき、最短で真実に辿り着けるのは「いま実行プロセスがどのDLLをロードしているか」を見ることです。Visual Studioには確認手段があります。
- デバッグ中に [デバッグ] → [ウィンドウ] → [モジュール] を開く
- 対象のDLLを探し、次を確認する
| 見る項目 | 何が分かるか | ありがちな落とし穴 |
|---|---|---|
| パス | 実行中プロセスがロードしているDLLの実体 | 想定と違うフォルダ(古いコピー)を読んでいる |
| シンボルの状態 | PDBが読み込まれているか/一致しているか | 別ビルドのPDBを拾っていて行番号がズレる |
| 更新日時(外部で確認) | そのDLLが本当に更新されているか | “自分が見ていたDLL”と“ロードされたDLL”が別物 |
この確認をすると、原因が「ビルド漏れ」なのか「ロード先違い」なのかが一気に絞れます。
一時的な対処:DLL削除で直る理由と、正しい使い方
問題が起きたときに「DLLを削除してからビルドすると直る」ことがあります。これは気休めではなく、理屈があります。
- 出力DLLが存在しないと、ビルドシステムは「生成が必要」と判断しやすい
- 結果としてリンク(またはコピー)工程が強制され、最新のobj/libが取り込まれる
- 古いDLL/PDBの組み合わせが消えるため、ブレークポイントが復活する
ただし、削除で直るのは“症状の解消”であって、“原因の解消”ではありません。再発防止のためには、F5時のビルド対象や出力先の整合性を直す必要があります。
切り分けチェックリスト:再発時に迷わない手順
大規模C++ソリューションでは、原因が1つとは限りません。再発したとき用に、短いチェックリストを用意しておくと強いです。
| 手順 | 確認すること | 判断の目安 | 次アクション |
|---|---|---|---|
| 1 | デバッグ中の「モジュール」でDLLのパスを確認 | 想定と違う場所ならロード先違い | 配置・コピー手順、作業ディレクトリ、PATHを見直す |
| 2 | 同じ画面でPDB(シンボル)が読み込まれているか確認 | 未読込/不一致ならブレークがズレやすい | シンボル設定、出力フォルダ、クリーン&再ビルド |
| 3 | F5時のビルド設定(Only build…)がONか確認 | ON+スタートアップがlibなら危険 | 設定をOFF、またはスタートアップをホストexeへ変更 |
| 4 | プロジェクトの依存関係(ビルド順序)を確認 | 依存が未定義だと更新が伝播しない | [プロジェクトの依存関係]で見直す |
| 5 | DLLロック(他プロセスが掴む)を疑う | ロックなら通常はリンクエラーが出やすい | Process Explorer等でハンドル確認、常駐プロセスを洗う |
再発防止の実務テクニック:大規模C++ DLL開発で効く工夫
原因(Only build…の設定)を直しても、プロジェクト規模が大きいと別の要因で「更新されたはずなのに止まらない」を踏みがちです。運用として効くポイントをまとめます。
スタートアップは「実際に起動するexe」に寄せる
- 基本はホストexeをスタートアップにする
- プラグイン開発でDLLをスタートアップにする場合も、staticライブラリは避ける
- 「F5を押した人が、いま何を起動しているか」まで含めて分かりやすくなる
依存関係・出力先を“単純に”する
ビルドは成功しているのに実行時が古い、という事故は「出力先が複数ある」「コピー工程がある」ほど起きやすくなります。
| 観点 | おすすめ | 理由 |
|---|---|---|
| 出力ディレクトリ | 構成(x64 Debugなど)ごとに一意のフォルダに統一 | “別のDLLを見ていた”事故を減らす |
| 中間生成物 | プロジェクト別に分離 | 同名objなどの衝突を避ける |
| ポストビルドコピー | 必要最小限にし、コピー先を明示してログに残す | 「ビルドはA、実行はB」を作りにくくする |
| 依存関係 | [プロジェクトの依存関係]で明示 | “更新が伝播しない”を防ぐ |
デバッグ情報を確実に生成・一致させる
ブレークポイント問題は、最終的には「ロードしたDLL」と「PDB」が一致しているかに帰着します。Debug構成では次を意識すると安定します。
- DLL側でデバッグ情報が生成される設定になっているか(PDBが出る)
- ホストexeが参照するDLLと同じ場所のPDBをデバッガが拾えるか
- 別構成(Release)を誤って起動していないか
「ブレークポイントが無効」=「コードが違う」ではなく、「デバッガが見ている世界(DLL/PDB)とエディタが見ている世界(ソース)がズレている」ことがほとんどです。ズレを潰す作業を、仕組みとしてやりやすくしておくのが再発防止につながります。
F5とF7の違いを押さえると、今後ハマりにくい
最後に、今回の根本原因につながるポイントを整理します。
| キー操作 | 主目的 | やること | 落とし穴 |
|---|---|---|---|
| F7(ビルド) | ビルドする | ビルド設定に従ってコンパイル・リンク | 構成や出力先を取り違えると“ビルドしたのに古い”が起きる |
| F5(デバッグ開始) | 実行してデバッグする | 必要なビルドを行い、起動してアタッチ | Only build…がONだとビルド対象が限定され、更新漏れが起きる |
| Shift+F5 | デバッグ停止 | プロセス終了(ただし別プロセスがDLLを掴む可能性は残る) | ホストが落ちても、別常駐が掴んでいると更新できないことがある |
大規模ソリューションほど、「F5の“便利な省略”」が裏目に出たときの影響が大きくなります。今回の設定はまさにその代表例です。
まとめ:DLLが更新されないときは「F5のビルド範囲」と「ロード先」を疑う
- 「ソースコードが元のバージョンと異なります」は、DLL/PDBとソースの不一致サイン
- 原因として多いのが、F5時にスタートアップ+依存関係のみをビルドする設定がONになっていること
- 特に、スタートアップがstaticライブラリだとDLLは“依存関係”ではなく“依存される側”なので、F5では更新されにくい
- 設定をOFFにする、またはスタートアップをホストexeにすることで安定しやすい
- 再発時は「モジュール」ウィンドウでロードされたDLLのパスとPDBを最優先で確認する

コメント