Visual Studio 2019でC++ DLLが更新されない・ブレークポイントが無効になる原因と解決策(Only build startup projects and dependencies on Run)

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が一致していないといった複数のパターンで発生します。

再現しやすい流れと、起きていること

典型的には次のような流れで遭遇します。

  1. デバッグ中のホストアプリ(exe)を Shift + F5 で停止
  2. 共通staticライブラリ側のソース(.cpp)を少し修正
  3. F7 でビルド(staticライブラリは再ビルドされ、出力ログ上はDLLもリンクされたように見える)
  4. F5 でデバッグ開始
  5. 変更箇所に置いたブレークポイントが「ソースコードが元のバージョンと異なります」と表示され無効化

状況をよく観察すると、次のような“矛盾”が見つかります。

  • 該当.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にすることです。

  1. Visual Studioのメニューから [ツール] → [オプション] を開く
  2. [プロジェクトおよびソリューション] → [ビルドおよび実行] を開く
  3. 「実行時にスタートアップ プロジェクトと依存関係のプロジェクトのみをビルドする(Only build startup projects and dependencies on Run)」 のチェックをオフにする
  4. 必要なら一度ソリューションをビルドし直し(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(シンボル)が読み込まれているか確認未読込/不一致ならブレークがズレやすいシンボル設定、出力フォルダ、クリーン&再ビルド
3F5時のビルド設定(Only build…)がONか確認ON+スタートアップがlibなら危険設定をOFF、またはスタートアップをホストexeへ変更
4プロジェクトの依存関係(ビルド順序)を確認依存が未定義だと更新が伝播しない[プロジェクトの依存関係]で見直す
5DLLロック(他プロセスが掴む)を疑うロックなら通常はリンクエラーが出やすい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を最優先で確認する

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次