Debug Assertion Failed! の直し方:File・Lineから失敗箇所を調べる【MFC / C++】

【MFC】Debug Assertion Failed! の正しい読み方

Debugビルドで実行中に突然「Debug Assertion Failed!」というダイアログが表示されて、何をどう読めばいいか分からず閉じてしまった、という経験は珍しくありません。

ダイアログのFileとLineを確認し、該当するソースを開きます。ASSERTの条件式と変数の値を見ながら、なぜ条件を満たさなかったのか調べます。


目次

最初に見る場所:FileとLine

たとえ条件式が表示されなくても、FileとLineからソースを開けば、ASSERTに書かれた条件式と変数の値を調べられます。MFCのASSERTでは、失敗した条件式を示すExpression欄は出ません。CRTの_ASSERTEなど、条件式を表示する別のマクロと区別してください。

項目意味見方
FileASSERTが書かれているソースファイル名(設定によってはフルパス)MFC内部のファイルか、自分のコードかを確認する
LineASSERTマクロが置かれている行番号Fileと合わせてソースを開く手がかりになる

たとえば次のように表示されたとします。

Debug Assertion Failed!

File: f:\dd\vctools\vc7libs\ship\atlmfc\src\mfc\wincore.cpp
Line: 981

この表示なら、使用中のMFCに対応するwincore.cppの981行目を開きます。そこでASSERTの条件式を確認します。MFCの版によって行番号や実装は変わるため、別の版の同じ行番号だけを見て原因を決めないでください。

ダイアログの1行目が「Debug」の付かない「Assertion failed!」で、Expressionの行に式が出ている場合は、MFCのASSERTではなくC/C++標準のassert()が失敗しています。2つのダイアログの見分け方と、assert()のダイアログの読み方は「Assertion failed!の読み方:assert()とDebug Assertion Failed!の見分け方」で扱っています。


ダイアログのボタンの意味

ダイアログのボタンは3つ、中止 (Abort)・再試行 (Retry)・無視 (Ignore) です。押す順番を間違えると原因箇所を見失います(中止を押すとアプリがその場で終わるので、FileとLineを控える前なら手がかりも一緒に消えます)。

ボタン動作推奨
中止 (Abort)アプリを即座に終了する原因を調べたいなら押さない
再試行 (Retry)デバッグブレークを発生させる。接続中のデバッガーで停止するVisual Studioからデバッグ実行中なら、調査のために選ぶ
無視 (Ignore)ASSERTを無視して続行する不安定な状態で続行するため非推奨

Visual Studioからデバッグ実行している場合は、「再試行 (Retry)」で停止して、呼び出し履歴と変数を確認します。停止位置がMFC内部やブレーク処理になった場合は、呼び出し履歴からASSERTを実行したフレームへ移動します。アプリを単独起動した場合、Visual Studioで調べるにはデバッガーへの接続やJITデバッグの設定が必要です。再試行だけで必ずVisual Studioが開くわけではありません。

MFCのASSERT失敗ダイアログ。FileとLineが表示され、Expression欄はない

FileがMFC内部のソースだった場合

ダイアログのFileがwincore.cpp・viewcore.cpp・dlgcore.cppや、そのほかのMFC内部のファイルを示していることがあります。まず、MFCへ渡した値や、ウィンドウ・モジュールの状態を調べます。MFC内部で検出されたというだけで、原因がMFCにあるか、自分のコードにあるかは決まりません。

この場合は、コールスタック(呼び出し履歴)を下に辿って、自分のコードが最初に現れる行を探してください。その呼び出し元を調査の入口にします。不正な値や状態は、もっと前の処理で作られている場合もあります。

  1. 「再試行」でデバッガーに入る
  2. コールスタックウィンドウを開く(デバッグ → ウィンドウ → 呼び出し履歴、またはCtrl + Alt + C)
  3. スタックの上から下へ順に見て、自分のソースファイル名が出てくるフレームをダブルクリック
  4. その行の前後で、NULLになっている変数や不正な値を渡している箇所を確認する

コールスタックの読み方について詳しくは、【MFC】Call Stack (呼び出し履歴) の歩き方を参照してください。


よくあるAssertion失敗パターン

MFCのDebugビルドでよく遭遇するAssertionのパターンを整理します。FileとLineからソースを開き、ASSERTに書かれた条件式で調査する対象を絞ります。以下の条件式は例で、実際の式や行番号はMFCの版と呼び出し経路によって異なります。

① pWnd != NULL / pMap != NULL

pWndはその箇所で扱うウィンドウオブジェクト、pMapはHWNDとMFCオブジェクトを対応付けるハンドルマップ自体を指します。pMapがNULLであることと、マップ内に特定のHWNDの対応がないことは別です。実際のASSERT位置を確認し、前者ならモジュール・スレッドの状態、後者ならウィンドウの作成時期やハンドルを調べます。以下はウィンドウオブジェクト側の確認例です。

  • OnInitDialogより前にコントロールにアクセスしている
  • ダイアログが閉じたあとにメンバー変数のコントロールを操作している
  • GetDlgItemの戻り値を確認せず使っている

② afxCurrentInstanceHandle != NULL

MFCのモジュール状態が正しくセットされていません。DLL内でMFCを使っている場合によく発生します。

  • MFCへ動的リンクする通常MFC DLLでは、エクスポート関数の先頭にAFX_MANAGE_STATE(AfxGetStaticModuleState())が必要です。静的リンクする通常MFC DLLやMFC拡張DLLへ一律に追加する対処ではありません

③ IsWindow(m_hWnd)

m_hWndが、現在有効なウィンドウを指していません。まだ作成していないNULLハンドル、破棄後のハンドルなどを確認します。IsWindowの失敗だけでは、破棄されたかどうかまでは分かりません。

  • ダイアログをDestroyWindowした後にメンバー関数を呼んでいる
  • タイマーコールバックが、ウィンドウ破棄後に走っている

④ nIndex >= 0 && nIndex < m_nSize

MFCのコレクションクラス(CArray / CObArrayなど)で、配列の範囲外にアクセスしています。

  • ループの終了条件がGetSize() を超えている
  • 要素を削除したあとにインデックスを更新していない

Releaseビルドでは出ない理由

ASSERTマクロはDebugビルドでのみ有効です。Releaseビルドではプリプロセッサによって完全に消えます。

// MFC の ASSERT マクロ(簡略化)
#ifdef _DEBUG
  #define ASSERT(f) \
    do { \
        if (!(f) && AfxAssertFailedLine(THIS_FILE, __LINE__)) \
            AfxDebugBreak(); \
    } while (0)
#else
  #define ASSERT(f) ((void)0)
#endif

ReleaseではASSERTの条件式も評価されません。ダイアログが出なくなっても、その後の処理が安全だと確認されたことにはなりません。NULLポインタを参照する処理や配列の範囲外アクセスが実際に残っていれば、クラッシュや未定義動作につながります。

DebugとReleaseでは、条件式の評価や実行経路が異なる場合があります。DebugのASSERT失敗から、Releaseでも同じ障害が必ず起きるとは断定できません。ASSERTが失敗した理由を調べ、必要な修正を行ったうえで両方の構成を確認してください。


出力ウィンドウのASSERTメッセージ

Visual Studioの出力ウィンドウにもASSERTの報告が出ている場合は、ファイル名と行番号を保存できます。ただし、報告先の設定によってはダイアログだけに出るため、中止したあとも必ず出力に残るとは限りません。

CRT経由のアサーション報告をダイアログとデバッガー出力の両方へ送るには、Debugの初期化処理で次のように設定します。アプリ全体の報告先を変える設定なので、調査用として使用してください。使用するMFCの実装や独自の報告フックによっては、別の報告経路になる場合もあります。

#ifdef _DEBUG
#include <crtdbg.h>
// Debugの初期化処理に書く
_CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_DEBUG | _CRTDBG_MODE_WNDW);
#endif

報告先の動作はMicrosoft Learnの_CrtSetReportModeを参照してください。出力がない場合は、ダイアログを閉じる前にFileとLineを控えてから調べます。


ASSERTを自分のコードに仕込む

MFCが内部で使っているのと同じASSERTマクロは、自分のコードでも使えます。「ここにNULLが来たらおかしい」「この値は正の整数のはず」という前提条件をコードに埋め込んでおくと、不正な状態を早期に発見できます。

void CMyDialog::SetItemCount(int nCount)
{
    // 負の値は前提違反。Debug で報告し、再試行で停止できる
    ASSERT(nCount >= 0);

    m_nItemCount = nCount;
    UpdateList();
}

ASSERTの詳しい活用法については、【MFC】ASSERT(条件) でバグを早期発見するを参照してください。


まとめ

  • [ ] ダイアログのFileとLineからソースを開き、ASSERTの条件式を確認する
  • [ ] デバッガー接続中に「再試行 (Retry)」で停止し、呼び出し履歴と変数を調べる
  • [ ] FileがMFC内部のソースなら、コールスタックを下に辿って自分の呼び出し元を特定する
  • [ ] ReleaseでASSERTが出なくても安全とは判断せず、失敗原因を調べて両方の構成で確認する
目次