Release ビルドでだけ再現するバグは、Debug 構成に戻しても原因に近づけません。ログを仕込もうにも TRACE は Release では消え、デバッガをアタッチしてもウォッチの値が最適化で見えないことがあります。
この記事では、Release ビルドのまま原因を追う 3 つの手段――OutputDebugString + DebugView によるログ取得、/Zi + /DEBUG による PDB 出力、/Od や #pragma optimize による部分的な最適化無効化――を、設定手順・コード例・トラブルシュートまで網羅します。
状況ごとの判断フロー
Release ビルドのバグ調査で最初に確認するべき手段を整理します。上から順に試してください。
| やりたいこと | 使う手段 | 準備 | 注意点 |
|---|---|---|---|
| Release でも実行中のログを見たい | OutputDebugString + DebugView | DebugView をインストール、Capture Win32 を ON | TRACE は Release で消えるため使えない |
| クラッシュ位置を行番号で追いたい | /Zi + /DEBUG で PDB を出力 | プロジェクトプロパティ 2 箇所を変更 | PDB を exe と同じビルドで保管する |
| 変数の値がウォッチで表示されない | /Od または #pragma optimize("", off) | 対象関数だけに限定する | 調査後は必ず元に戻す |
| Release だけ挙動が変わる原因を切り分けたい | 未初期化変数・ASSERT 内副作用・タイミング依存を確認 | ログに入力値と分岐結果を残す | Debug で再現しない前提で切り分ける |
| ユーザー環境のクラッシュを後から追いたい | PDB + ダンプファイル解析 | シンボルサーバまたはローカル PDB 保管 | ビルド番号と PDB の対応を管理する |
この記事で使う開発環境
| OS | Windows 11 |
| IDE | Visual Studio 2026 |
| ワークロード | C++ によるデスクトップ開発 |
| MFC | MFC / ATL サポートを有効 |
| 構成 | x64 / Release |
| 確認ツール | DebugView v4.90 以降 (Sysinternals) |
手段1:OutputDebugString + DebugView でログを取る
Release ビルドでログを残す最も手軽な方法は、Windows API の OutputDebugString です。MFC の TRACE マクロは Debug 構成でしか動きません。Release 構成では TRACE が空に展開されるため、何も出力されません。
TRACE と OutputDebugString の違い
| 項目 | TRACE (MFC マクロ) | OutputDebugString (Win32 API) |
|---|---|---|
| Release で動くか | 動かない(空に展開される) | 動く |
| 出力先 | Visual Studio 出力ウィンドウ | デバッガ出力ウィンドウ / DebugView |
| デバッガなしで見れるか | 見れない | DebugView で見れる |
| パフォーマンス影響 | Release では無し | 呼び出しごとにカーネルオブジェクト経由 |
| 書式付き出力 | TRACE(_T("x=%d"), x) | 自前でフォーマットが必要 |
基本的な使い方
// Release でも DebugView で確認できるログ出力
CString msg;
msg.Format(_T("[MyApp] value=%d, state=%d\n"), value, state);
::OutputDebugString(msg);
プレフィックス(例: [MyApp])を付けておくと、DebugView のフィルタ機能で自分のログだけを抽出できます。システム全体のデバッグ出力が混ざる環境では必須です。
書式付きヘルパー関数を用意する
毎回 CString::Format + OutputDebugString を書くのは冗長です。TRACE と同じ感覚で使えるヘルパーを作ります。
// ReleaseTrace.h — Release でも使える書式付きログ出力
#pragma once
static void ReleaseTrace(LPCTSTR format, ...)
{
va_list args;
va_start(args, format);
CString message;
message.FormatV(format, args);
::OutputDebugString(message);
va_end(args);
}
呼び出し側は TRACE と同じ書き方です。
ReleaseTrace(_T("[MyApp] OnTimer: id=%d, elapsed=%dms\n"), nIDEvent, elapsed);
ReleaseTrace(_T("[MyApp] CreateFile failed: path=%s\n"), filePath);
条件付きで出力を切り替える
調査が終わったら OutputDebugString の呼び出しを残したくない場合は、プリプロセッサで制御します。
// RELEASE_TRACE_ENABLED を定義したときだけ有効にする
#ifdef RELEASE_TRACE_ENABLED
#define RTRACE(fmt, ...) ReleaseTrace(fmt, __VA_ARGS__)
#else
#define RTRACE(fmt, ...) ((void)0)
#endif
プロジェクトプロパティの C/C++ → プリプロセッサ → プリプロセッサの定義 に RELEASE_TRACE_ENABLED を追加すれば有効になります。出荷時は定義を外すだけで全出力が消えます。
DebugView の起動と設定
DebugView は Sysinternals の無料ツールです。OutputDebugString の出力をリアルタイムで表示します。
- Microsoft Learn の Sysinternals ページから DebugView をダウンロードします
Dbgview.exeを起動します- メニュー Capture → Capture Win32 にチェックを入れます(
Ctrl + W) - この状態で Release ビルドの exe を実行します
OutputDebugStringの出力がリアルタイムに表示されます
Visual Studio のデバッガをアタッチしている場合は、OutputDebugString の出力は Visual Studio 側が受け取ります。DebugView で見たい場合は、デバッガなしで実行してください。
![DebugView の画面。[ReleaseTrace] で始まるログが複数行表示されている。Capture Win32 が有効の状態](https://www.mfcguide.com/wp-content/uploads/release-mode-debug-settings-optimization-03.png)
DebugView のフィルタ設定
システム全体のデバッグ出力が大量に流れる環境では、フィルタを設定します。
- メニュー Edit → Filter/Highlight を開きます(
Ctrl + L) - Include に自分のプレフィックスを入力します(例:
[MyApp]) - OK を押すと、プレフィックスを含む行だけが表示されます
Highlight 欄に色付けルールを追加すると、エラーログだけ赤で表示するような使い方もできます。
![DebugView の Filter/Highlight ダイアログ。Include 欄に [ReleaseTrace] と入力し、自分のログだけを抽出している状態](https://www.mfcguide.com/wp-content/uploads/release-mode-debug-settings-optimization-04.png)
DebugView にログが出ないときの確認項目
「OutputDebugString を入れたのに DebugView に何も出ない」は頻出のトラブルです。原因は 5 パターンに絞れます。
| 原因 | 確認方法 | 対処 |
|---|---|---|
| Capture Win32 が OFF | DebugView のメニュー Capture を確認 | Ctrl + W で ON にする |
| Visual Studio デバッガがアタッチ済み | VS の出力ウィンドウにログが出ていないか確認 | デバッガなしで実行(Ctrl + F5)するか、VS を閉じる |
| 権限の不一致 | アプリが管理者権限で動いている | DebugView も管理者権限で起動する |
| 64bit / 32bit の不一致 | 64bit アプリに対して 32bit の DebugView を使用 | 64bit 版 Dbgview64.exe を使う |
| フィルタで除外されている | Filter/Highlight の Include / Exclude を確認 | 一度フィルタをクリアして確認する |
最も多いのは「Visual Studio からデバッグ実行しているため、デバッガが OutputDebugString を横取りしている」パターンです。DebugView で確認するときは、デバッガなしで実行してください。
手段2:/Zi と /DEBUG で PDB を出力し、行番号を追う
ログだけで原因を絞れない場合は、Release 構成でも PDB(プログラムデータベース)を出力します。PDB があれば、クラッシュダンプやデバッガ上でシンボル名、関数名、行番号を追えます。
設定手順
- プロジェクトのプロパティを開きます
- 上部の構成を Release、プラットフォームを x64 にします
- C/C++ → 全般 → デバッグ情報の形式 を
プログラム データベース (/Zi)にします - リンカー → デバッグ → デバッグ情報の生成 を
はい (/DEBUG)にします - リンカー → 最適化 → 参照 を
/OPT:REFに明示します - リンカー → 最適化 → COMDAT の圧縮 を
/OPT:ICFに明示します

/Zi と /DEBUG の関係
| 設定 | 役割 | 片方だけだと |
|---|---|---|
/Zi(C/C++ コンパイラ) | 各 .obj にデバッグ情報を埋め込む | リンク後の PDB が生成されない |
/DEBUG(リンカー) | .obj のデバッグ情報を集約して PDB を生成 | 参照するデバッグ情報がないため PDB が空になる |
両方を設定して初めて、行番号やシンボル名を含む PDB が生成されます。
/DEBUG を付けたときのリンカー最適化
/DEBUG を指定すると、リンカーの既定動作が変わります。未参照関数やデータの除去(/OPT:REF)と同一 COMDAT の折りたたみ(/OPT:ICF)が無効になり、Release ビルドの実行ファイルサイズが増えます。
Release の実行動作になるべく近い状態で調査したい場合は、/OPT:REF と /OPT:ICF を明示的に設定します。これにより、PDB は出力されつつ、実行コードは通常の Release に近い状態を維持します。
// .vcxproj 内の該当箇所(参考)
<Link>
<GenerateDebugInformation>true</GenerateDebugInformation>
<OptimizeReferences>true</OptimizeReferences>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
</Link>
PDB の管理と保管
PDB は実行ファイルと 1 対 1 で対応します。異なるビルドの PDB を当てると、行番号がずれます。
- 開発中: ビルド出力フォルダに PDB が自動生成されるため、そのまま使えます
- 社内配布: リリースごとに PDB をバージョン付きフォルダに保管します
- ユーザー環境のクラッシュ: ダンプファイル(.dmp)を回収し、対応する PDB を使って Visual Studio で解析します
PDB をユーザーに配布する必要はありません。PDB はソースコードのパスや内部構造を含むため、外部公開しない場所に保管してください。
手段3:最適化を部分的に切って変数を追う
Release 構成では、最適化によって変数がレジスタに格納されたり、関数がインライン展開されたりします。デバッガのウォッチで 「最適化により使用不可」 と表示される場合、一時的に最適化を切って調査します。
方法A:#pragma optimize で関数単位に切る
プロジェクト全体の最適化を切ると、Release の再現条件が変わることがあります。最初は疑わしい関数だけに限定します。
#pragma optimize("", off)
void CMyDialog::OnProcessData()
{
// この関数だけ最適化を抑えて調査する
int result = CalculateValue(m_input);
// ここで result の値をウォッチで確認できる
if (result < 0) {
// Release ではここに来るのに Debug では来ない、を追跡する
ReleaseTrace(_T("[MyApp] result=%d (unexpected)\n"), result);
}
}
#pragma optimize("", on)
方法B:プロジェクト全体を /Od にする
関数単位で絞り込めない場合は、プロジェクト全体の最適化を切ります。
- C/C++ → 最適化 → 最適化 を
無効 (/Od)に変更します - ビルドして現象を再現します
/Odで再現しなくなった場合は、最適化依存のバグです/Odでも再現する場合は、未初期化変数や論理エラーの可能性が高くなります
方法C:特定ファイルだけ最適化を変える
ソリューションエクスプローラーで特定の .cpp ファイルを右クリックし、プロパティを開きます。そのファイルだけ /Od に変更できます。プロジェクト全体の設定は変わりません。
重要: /Od や #pragma optimize("", off) は調査用です。原因が分かったら必ず元に戻してください。調査設定のまま出荷すると、パフォーマンスが落ちるだけでなく、最適化依存の問題が隠れたまま残ります。
Release だけで起きるバグの典型パターン
Release ビルド固有のバグは、パターンごとに追い方が決まっています。闇雲にログを入れる前に、どのパターンに該当するかを判断します。
| パターン | Debug ではなぜ見えないか | Release での症状 | 追い方 |
|---|---|---|---|
| 未初期化変数 | Debug ランタイムがメモリを 0xCC 等で埋めるため、偶然動くことがある | 不定値で分岐が変わる、アクセス違反 | 初期化漏れをコード検索、ログで入力値と分岐を残す |
| ASSERT 内の副作用 | Debug で ASSERT の中身が評価される | Release で ASSERT ごと消え、副作用も消える | ASSERT 内の関数呼び出し・代入を外に出す |
| タイミング依存(競合) | Debug は最適化なしで実行が遅く、競合が表面化しにくい | 最適化で実行時間が変わり、競合や待ち合わせ漏れが出る | スレッド ID、時刻、状態遷移を OutputDebugString で残す |
| 最適化による再配置 | Debug はコード順に実行される | コンパイラが処理順を変え、volatile でない変数の読み取りが期待と異なる | #pragma optimize で範囲を絞り、volatile を検討する |
| Release 専用のマクロ差異 | Debug で有効なマクロ(_DEBUG, ASSERT)が Release で消える | Debug 専用パスに依存した処理が丸ごと消失 | プリプロセッサ展開結果を確認する |
原因そのものの切り分けは「【MFC】Releaseビルドだけ落ちる: 最適化と未初期化変数の確認手順」で詳しく扱っています。
実践:Release ビルドのバグ調査フロー
実際の調査手順を時系列で並べます。
ステップ1:現象を確認する
- Release ビルドで現象が確実に再現するか確認します
- Debug ビルドで同じ操作をして再現しないことを確認します
- 再現条件(操作手順、データ、タイミング)を記録します
ステップ2:ログを仕込む
- 疑わしい箇所の入口と出口に
OutputDebugStringを入れます - 変数の値、分岐の結果、関数の戻り値をログに含めます
- DebugView で出力を確認しながら範囲を狭めます
void CMyDialog::OnProcess()
{
ReleaseTrace(_T("[MyApp] OnProcess: start, m_count=%d\n"), m_count);
int result = DoCalculation(m_data, m_count);
ReleaseTrace(_T("[MyApp] OnProcess: DoCalculation returned %d\n"), result);
if (result == 0) {
ReleaseTrace(_T("[MyApp] OnProcess: unexpected zero result\n"));
return; // ← Release だけここに来る?
}
int divided = 100 / result;
ReleaseTrace(_T("[MyApp] OnProcess: divided=%d\n"), divided);
}
ここまでのコードを Release ビルドで実行し、DebugView を開いた状態で問題の操作を再現すると、[MyApp] プレフィックス付きのログがリアルタイムで流れます。

ステップ3:PDB でクラッシュ位置を特定する
- ログで範囲を絞れない場合は、
/Zi+/DEBUGを設定してビルドします - Visual Studio から Release 構成でデバッグ実行(
F5)します - クラッシュ時に行番号とコールスタックを確認します
ステップ4:最適化を切って変数を確認する
- 変数がウォッチで見えない場合は、その関数だけ
#pragma optimize("", off)を付けます /Odで現象が消える場合は、最適化依存のバグと判断します- 原因が分かったら、
#pragma optimizeと/Odを戻します
OutputDebugString のパフォーマンスと出荷時の扱い
OutputDebugString は呼び出しごとにカーネルオブジェクトを経由するため、高頻度ループ内で使うと性能に影響します。
| 状況 | 影響度 | 対応 |
|---|---|---|
| ボタン押下時に数行出す | 無視できる | 出荷版でも残しておいてよい |
| 描画ループ内で毎フレーム出す | 目に見える遅延 | 条件付き(N フレームに 1 回)か、出荷時は除去 |
| ファイル I/O ループ内で毎行出す | I/O 性能に直接影響 | 調査終了後に除去、またはプリプロセッサで制御 |
出荷時に全ての OutputDebugString を外したい場合は、前述の RTRACE マクロのようにプリプロセッサで制御するか、呼び出し側で #ifdef を使います。デバッガがアタッチされていない状態での OutputDebugString は、受け手がいなくてもわずかなオーバーヘッドがあります。
Release デバッグ設定の一覧(コピペ用)
Release 構成で調査用に変更する設定を一覧にまとめます。調査終了後にどこを戻すか確認するためのチェックリストとしても使えます。
| プロパティパス | 通常の Release 値 | 調査時の設定 | 戻し忘れリスク |
|---|---|---|---|
| C/C++ → 全般 → デバッグ情報の形式 | なし or /Zi | /Zi | 低(PDB が増えるだけ) |
| リンカー → デバッグ → デバッグ情報の生成 | なし | はい (/DEBUG) | 低(/OPT:REF 等を明示すれば影響小) |
| リンカー → 最適化 → 参照 | はい (/OPT:REF) | /OPT:REF(明示) | なし |
| リンカー → 最適化 → COMDAT の圧縮 | はい (/OPT:ICF) | /OPT:ICF(明示) | なし |
| C/C++ → 最適化 → 最適化 | /O2 | /Od | 高(出荷前に必ず /O2 に戻す) |
/Zi と /DEBUG は、Release の出荷版でも常時有効にしている現場があります。PDB を保管する運用があれば、ユーザー環境のクラッシュダンプをいつでも解析できるメリットがあり、実行時の性能には影響しません。
関連記事
- 【MFC】Releaseビルドだけ落ちる: 最適化と未初期化変数の確認手順 — Release 固有バグの原因切り分けに特化した記事
- 【MFC】ASSERT(条件) でバグを早期発見する — ASSERT の基本的な使い方と Release での注意点
- 【MFC】Call Stack (呼び出し履歴) の歩き方 — PDB 付きの Release ビルドでコールスタックを読む技術
- 【MFC】Memory Leak Detection: _CrtSetDbgFlag でリークを自動報告させる — Debug 構成でのリーク検出手法
まとめ
- Release でログを取るには
OutputDebugStringと DebugView を使います。TRACEは Release で空になるため使えません - DebugView にログが出ない場合は、Capture Win32 の ON/OFF、デバッガのアタッチ状態、管理者権限の差を確認します
- クラッシュ位置を行番号で追うには
/Zi+/DEBUGで PDB を出力し、/OPT:REFと/OPT:ICFを明示します - 変数がウォッチで見えないときは
#pragma optimize("", off)で関数単位に最適化を切ります。プロジェクト全体の/Odは最終手段です /Odや#pragma optimizeは調査用です。原因が分かったら必ず元に戻します/Ziと/DEBUGは出荷版でも有効にできます。PDB を保管しておけば、ユーザー環境のクラッシュダンプをいつでも解析できます
