Releaseビルドでだけ再現するバグは、Debug構成に戻しても原因に近づけません。ログを仕込もうにもTRACEはReleaseでは消え、デバッガをアタッチしてもウォッチの値が最適化で見えないことがあります。
この記事では、Releaseビルドのまま原因を追う3つの手段――OutputDebugString + DebugViewによるログ取得、/Zi + /DEBUGによるPDB出力、/Odや #pragma optimizeによる部分的な最適化無効化――を、設定手順・コード例・トラブルシュートまで網羅します。
ログを出すコードだけを確認したい場合は「OutputDebugStringで自作ログ出力」、デバッガのないPCで受信・保存する手順だけが必要なら「DebugViewでリリース後のログを見る」を先に参照してください。
未初期化変数、ASSERTの中の副作用、同期のない共有変数のように、Releaseビルドだけで落ちる原因の側から確かめたい場合は「【MFC】Releaseビルドだけ落ちる: 最適化と未初期化変数の確認手順」が先です。ここでは、原因の見当がまだつかないときに、ログ・PDB・最適化の切り替えで落ちる場所を絞り込みます。
状況ごとの判断フロー
ただログを足すだけで分かる不具合もあれば、PDBやダンプがないと追えない不具合もあります。やりたいことに合わせて、次の表の手段を上から順に試してください。
| やりたいこと | 使う手段 | 準備 | 注意点 |
|---|---|---|---|
| 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 / v5.0 (Sysinternals。掲載画像はv4.90) |
手段1:OutputDebugString + DebugViewでログを取る
ついTRACEでログを足したくなりますが、MFCのTRACEマクロはDebug構成でしか動きません。Release構成ではTRACEが空に展開されるため、何も出力されません。Releaseビルドでログを残すには、Windows APIのOutputDebugStringを使います。
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に何も出ないときは、次の表の原因を上から順に確かめます。
| 原因 | 確認方法 | 対処 |
|---|---|---|
| Capture Win32がOFF | DebugViewのメニューCaptureを確認 | Ctrl + WでONにする |
| Visual Studioデバッガがアタッチ済み | VSの出力ウィンドウにログが出ていないか確認 | デバッガなしで実行(Ctrl + F5)するか、VSを閉じる |
| 権限の不一致 | アプリが管理者権限で動いている | 必要に応じてDebugViewを昇格する |
| フィルタで除外されている | 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:NOICFにします。出荷版と同じ折りたたみ条件で再現確認するときだけ /OPT:ICFに戻します

/Ziと /DEBUGの関係
| 設定 | 役割 | 確認点 |
|---|---|---|
| /Zi(C/C++ コンパイラ) | 関数・変数・型・行位置などのシンボル情報をコンパイラPDBへ出力する | 生成した .objは対応するPDBの名前と署名を参照する |
| /DEBUG(リンカー) | 実行ファイルに対応するリンカーPDBを生成する | ソース行を追うにはコンパイラ側のデバッグ情報も必要 |
現在のMSVCでは /Ziが /DEBUGを暗黙に有効にしますが、プロジェクト設定を監査しやすくするため、ここではコンパイラとリンカーの両方を明示して確認します。
/DEBUGを付けたときのリンカー最適化
/DEBUGを指定すると、リンカーの既定動作が変わります。未参照関数やデータの除去(/OPT:REF)と同一COMDATの折りたたみ(/OPT:ICF)が無効になり、Releaseビルドの実行ファイルサイズが増えます。
Releaseの未参照コード除去を維持する場合は /OPT:REFを明示します。一方、/OPT:ICFは同一内容の関数を同じアドレスへまとめるため、スタックトレース、ブレークポイント、ステップ実行が分かりにくくなることがあります。調査開始時は /OPT:NOICFを使い、出荷版と同じ条件での再現が必要な段階だけ /OPT:ICFに切り替えます。
// .vcxproj 内の該当箇所(参考)
<Link>
<GenerateDebugInformation>true</GenerateDebugInformation>
<OptimizeReferences>true</OptimizeReferences>
<EnableCOMDATFolding>false</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++ → 全般 → デバッグ情報の形式 | プロジェクトによる | /Zi | 低(PDBが増える) |
| リンカー → デバッグ → デバッグ情報の生成 | プロジェクトによる | はい (/DEBUG) | リンカー最適化の既定値変更に注意 |
| リンカー → 最適化 → 参照 | はい (/OPT:REF) | /OPT:REF(明示) | なし |
| リンカー → 最適化 → COMDATの圧縮 | はい (/OPT:ICF) | /OPT:NOICF(調査時) | 出荷条件で再現するときは戻す |
| C/C++ → 最適化 → 最適化 | /O2 | /Od | 高(出荷前に必ず /O2に戻す) |
/Ziと /DEBUGは、Releaseの出荷版でも常時有効にしている現場があります。/Zi自体はコンパイラ最適化へ影響しません。PDBを保管するとユーザー環境のクラッシュダンプを解析できますが、/DEBUG指定時はリンカー最適化の既定値も確認してください。
関連記事
- 【MFC】OutputDebugString() で自作ログ出力 — Releaseでも残るログ出力コードに特化した記事
- 【MFC】DebugViewでリリース後のログを見る — デバッガなしで受信・保存・フィルタする手順
- 【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:NOICFを使い、必要な場合だけ出荷版と同じ /OPT:ICFへ戻します
- 変数がウォッチで見えないときは #pragma optimize("", off) で関数単位に最適化を切ります。プロジェクト全体の /Odは最終手段です
- /Odや #pragma optimizeは調査用です。原因が分かったら必ず元に戻します
- /Ziと /DEBUGは出荷版でも有効にできます。対応するPDBを保管し、リンカー最適化の設定も明示して管理します
