【MFC】Releaseビルドだけ落ちる: 最適化と未初期化変数の確認手順

【MFC】Releaseビルドだけ落ちる: 最適化と未初期化変数の確認手順

Debugビルドでは再現しないのに、Releaseビルドだけでクラッシュする場合は、設定より先にコード側の前提崩れを疑います。

最適化を切った構成で試すと落ちなくなることがあり、切り分けの手掛かりとしては便利です。ただ、それで直ったわけではありません。未初期化変数や同期のない共有変数が残っていれば、最適化を戻すとまた落ちることがあります。


目次

最初に切り分けるポイント

  • 最適化を切ったReleaseで挙動が変わるか
    → /Zi + /DEBUG + /Odで追える構成を作り、落ち方が変わるかを見ます。
  • 未初期化変数や副作用付きASSERTが残っていないか
    → ポインタ、整数、停止フラグの扱いを先に確認します。

原因1: 未初期化変数が残っている

ただ宣言しただけで値を入れていないポインタや整数を、そのまま比較や書き込みに使うコードが残っていると、Releaseビルドでだけ症状が変わります。

void CMyView::OnDraw(CDC* pDC)
{
    int* pData; // 初期化していないポインタ
    
    if (m_bUseData) {
        pData = new int[100];
        // ...
    }
    
    // m_bUseData が FALSE の時、ここでは pData は不定値
    if (pData != nullptr) { // 不定値に対する比較になっている
        pData[0] = 1;     // 不定ポインタに書き込んでいる
    }
}

Debugビルドでは表面化しやすい

Debugビルドでは、デバッグランタイムやコンパイラ設定の影響で、未初期化メモリが目立つ値に見えたり、アクセス違反として早めに表面化しやすくなります。MSVCでは0xCCCCCCCCのような値が見えることがあります。

Releaseビルドで不定値が残る理由

Releaseビルドではその補助が弱くなるため、不定値のまま処理が進みます。上記のpDataが不定値のまま比較や書き込みに使われると、アクセス違反やランダムクラッシュになります。

【対策】
ポインタや数値などの基本型は、ローカル変数として宣言した瞬間に必ず初期化する習慣をつけましょう。

// 宣言時に初期化する
int* pData = nullptr; 
int count = 0;

原因2: 最適化で実行条件が変わる

処理がいつ、どの順番で実行されるかは、Releaseビルドではソースの並びどおりとは限りません。Releaseビルドでは「コンパイラの最適化機能(/O2)」が有効になります。コンパイラは実行速度を最大化するために、不要と判断したコードブロックを丸ごと削除したり、処理の順序をプログラマの意図とは異なる形に並べ替えることがあります。

確認点A: ASSERTの中に副作用を入れていないか

ASSERTの中に状態変更を伴う処理を書くと、Releaseビルドではその処理自体が実行されなくなります。

// 注意: ASSERT 内で副作用を評価している
ASSERT( pMyObj->Initialize() == TRUE );

ASSERTマクロは、Debugビルドでは中身を評価し、エラーならダイアログを出します。しかし、ReleaseビルドではASSERT(...) という行そのものが消えます。
つまり、ReleaseビルドではpMyObj->Initialize() は呼ばれません。そのまま後続処理に進むため、初期化漏れとして落ちます。

【対策】
ASSERTの中には「副作用(変数の変更や関数呼び出し)」を書かないようにします。確認系メソッドは専用の変数で受けるか、Releaseビルドでも評価されるVERIFYマクロを使います。

// 例1: VERIFY を使う
VERIFY( pMyObj->Initialize() == TRUE );

// 例2: 変数で受ける
BOOL bInit = pMyObj->Initialize();
ASSERT( bInit == TRUE );

確認点B: 共有フラグを普通の変数で持っていないか

マルチスレッドで停止フラグや完了フラグを共有している箇所です。Debugでは動いて見えても、Releaseで急に止まらない、終了しない、値が見えない、という形で表面化します。

bool g_bStop = false;

// スレッドA
void ThreadA() {
    while (!g_bStop) {
        // 何か処理
    }
}

この書き方は、別スレッドからg_bStopを読む/書く時点で安全ではありません。Releaseでだけおかしく見えることがありますが、原因は「最適化に負けた」ではなく、共有変数を同期なしで使っていることです。

#include <atomic>

std::atomic<bool> g_bStop{ false };

// スレッドA
void ThreadA() {
    while (!g_bStop.load()) {
        // 何か処理
    }
}

// スレッドB
void RequestStop() {
    g_bStop.store(true);
}

【対策】
停止フラグはstd::atomic<bool> か、待機を伴うならCEventで扱います。volatileだけではスレッド間同期になりません。

MFCアプリでスレッドを待機させる場合は、ポーリングよりCEventで停止通知する方が素直です。大事なのは、普通のboolやvolatileに頼らないことです。


確認方法: Releaseビルドを追う

やみくもにReleaseビルドのままステップ実行しても、どこで落ちているかはなかなか絞り込めません。最適化でソース行と機械語がズレており、変数の値も「最適化されて利用できません」と表示されるためです。原因の見当がついていても、落ちている行を特定しなければ直せません。

そんな時は、Releaseビルドのまま、デバッグ情報を付ける(最適化を一時的に切る)構成を作ります。本番用のRelease設定を戻し忘れたくないなら、切り分け用にReleaseDebug構成を1つ作ってください。

プロジェクトプロパティでC/C++の「最適化」を「無効(/Od)」に設定している画面
  1. ソリューションをRelease構成にする。戻し忘れが怖い場合は、先にReleaseを複製して切り分け専用構成を作ります。
  2. 対象プロジェクトのプロパティを開く。
  3. C/C++ > 全般 > 「デバッグ情報の形式」 を プログラム データベース (/Zi) にする。
  4. リンカー > デバッグ > 「デバッグ情報の生成」 を はい (/DEBUG) にする。
  5. C/C++ > 最適化 > 「最適化」 を 無効 (/Od) にする。
  6. リビルドして実行する。

この状態で落ちなくなった場合は、最適化によるコードの削除や順序変更を確認します。
この状態でも落ちる場合は、未初期化変数やメモリアクセス違反を疑います。この構成ならデバッガでステップ実行と変数確認がしやすくなるため、クラッシュ行を特定して修正します。

/Odにすると落ちなくなって場所を追えないときや、Releaseの設定のまま場所を絞りたいときは、OutputDebugStringのログ、PDBを出力したReleaseビルド、#pragma optimizeで関数単位に最適化を切る方法を使います。手段ごとの設定と使う順番は「Releaseビルドだけで起きるバグの調べ方:DebugView / PDB / 最適化の切り分け」にまとめています。


まとめ

  • ローカル変数は宣言と同時に必ず初期化する(ポインタはnullptr、数値は0など)。
  • ASSERT() マクロの中に、関数呼び出しや代入文を書かない。必要ならVERIFY() を使う。
  • スレッド間の停止フラグはstd::atomic<bool> かCEventで扱う。
  • Releaseで落ちたら、/Zi + /DEBUG + /Odの切り分け構成を用意して確認する。

Releaseビルドだけで落ちる場合は、コンパイラではなくコード側の未定義動作や前提条件の崩れを確認します。未初期化変数と最適化の影響を分けて見ると、原因を絞り込みやすくなります。


関連記事

目次