【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ビルドでは「コンパイラの最適化機能(/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 で停止通知する方が素直です。大事なのは、普通の boolvolatile に頼らないことです。


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

原因は分かっても、「どこで落ちているか」を特定しなければ直せません。Releaseビルドでそのままデバッガをくっつけても、最適化でソース行と機械語がズレており、変数の値も「最適化されて利用できません」となりお手上げです。

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

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

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


まとめ

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

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


関連記事

目次