GetLastError は失敗直後に読む:別の API で値が変わる理由と書き方

【MFC】GetLastError() は失敗直後に読む:別APIでエラーコードが変わる理由

Win32 APIの呼び出しが失敗したとき、GetLastError() でエラーコードを取得するのは基本です。しかし、失敗直後に読まないと、別のAPIが設定したエラーコードを読んでしまうことがあります。
この記事では、GetLastError() をいつ読むべきかと、別APIを挟むと値が変わる理由を解説します。


目次

GetLastErrorの基本仕様

GetLastError() は、呼び出し元スレッドの最後に設定されたエラーコードを返します。Win32 APIの多くは、失敗時に内部でSetLastError() を呼んでエラーコードを設定します。

重要なのは、成功したAPIもエラーコードを上書きする場合があるという点です。つまり、失敗したAPIの直後に別のAPIを呼ぶと、エラーコードが消えたり、別のエラーコードに変わったりします。


失敗直後に読む

GetLastError() の値を元の失敗理由として使えるのは、APIが失敗を返した直後、他のAPIを一切呼ばずに取得した場合です。

// OK: 失敗の直後に GetLastError を呼ぶ
HANDLE hFile = CreateFile(_T("C:\\nonexistent.txt"),
    GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
    DWORD err = GetLastError();  // ← ここで取得する
    // err == ERROR_FILE_NOT_FOUND (2)
}

別APIを挟むと値が変わる

ケース1:別のAPIを挟んでしまう

// NG: 別の API を挟んでからエラーコードを取得
HANDLE hFile = CreateFile(_T("C:\\nonexistent.txt"),
    GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
    // ↓ この OutputDebugString が内部で SetLastError を呼ぶ可能性がある
    OutputDebugString(_T("ファイルが開けませんでした\n"));

    DWORD err = GetLastError();  // ← 元の失敗理由とは限らない
}

OutputDebugString、TRACE、wprintfなど、一見「エラーと無関係」な処理でも内部でWin32 APIを呼ぶことがあり、その結果エラーコードが上書きされる可能性があります。

ケース2:成功時のエラーコードを読む

// NG: API が成功したのに GetLastError を呼ぶ
BOOL ok = DeleteFile(_T("C:\\temp\\test.txt"));
DWORD err = GetLastError();  // ok == TRUE でも err に古い値が残っている

// ↑ 成功時の err は意味がない(前回のエラーコードが残っているだけ)

APIが成功した場合、GetLastError() の値は不定です。成功時にエラーコードを0にクリアするAPIもあれば、何もしないAPIもあります。必ず「失敗を確認してから」呼んでください。

ケース3:APIが「成功 + エラーコード」を返す場合

一部のAPIは、成功であっても追加情報をエラーコードに設定します。代表例はCreateMutexです。

HANDLE hMutex = CreateMutex(NULL, TRUE, _T("MyMutex"));
if (hMutex != NULL)
{
    // 作成には成功したが、既に存在していた場合
    if (GetLastError() == ERROR_ALREADY_EXISTS)
    {
        // 2つ目のインスタンスが起動された
    }
}

このようなAPIでは、ドキュメントに「成功時でもGetLastErrorを確認せよ」と明記されています。APIごとのドキュメントを確認してください。


安全なパターン:即座に保存する

エラーコードを確実に捕捉するには、失敗判定の直後にローカル変数に保存するのが鉄則です。

HANDLE hFile = CreateFile(path, GENERIC_READ, 0, NULL,
                           OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
    DWORD dwErr = GetLastError();   // ← 即座に保存

    // 以降は dwErr を使う(GetLastError は二度と呼ばない)
    CString msg;
    msg.Format(_T("CreateFile 失敗: エラーコード %lu"), dwErr);
    AfxMessageBox(msg);
    return;
}

FormatMessageでエラーコードを文字列に変換する

エラーコードの数値だけではユーザーに伝わりません。FormatMessageを使えば、OSが持つエラーメッセージ文字列に変換できます。

void ShowErrorMessage(DWORD dwErr)
{
    LPTSTR pMsg = NULL;
    FormatMessage(
        FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
        FORMAT_MESSAGE_IGNORE_INSERTS,
        NULL, dwErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
        (LPTSTR)&pMsg, 0, NULL);

    if (pMsg)
    {
        // 例: "指定されたファイルが見つかりません。"
        AfxMessageBox(pMsg);
        LocalFree(pMsg);
    }
}

※ FORMAT_MESSAGE_ALLOCATE_BUFFERで確保されたメモリはLocalFreeで解放してください。


デバッガでのエラーコード確認

Visual Studioのデバッガには、GetLastError() をコードに書かなくてもエラーコードを確認する方法があります。

ウォッチウィンドウに @err,hrと入力してください。現在のスレッドの最終エラーコードと、そのHRESULT形式の説明が表示されます。

ブレークポイントで停止した時点のエラーコードがリアルタイムで確認できるため、GetLastError() の呼び出しタイミングを気にせずに原因を追えます。


SetLastError(0) で事前クリアする手法

APIのドキュメントに「成功時でもエラーコードが設定される」と書かれている場合は、事前にSetLastError(0) でクリアしてからAPIを呼ぶことで、エラーの有無を明確に判定できます。

SetLastError(0);
LONG prev = SetWindowLongPtr(hWnd, GWL_STYLE, newStyle);
if (prev == 0)
{
    DWORD err = GetLastError();
    if (err != 0)
    {
        // 本当に失敗した
    }
    // err == 0 なら、以前の値が 0 だっただけ(成功)
}

SetWindowLongPtrは、以前の値が0の場合と失敗の場合の両方で0を返します。事前にSetLastError(0) を呼ぶことで区別できます。


まとめ

  • GetLastError() は失敗したAPIの直後に、他のAPIを挟まずに呼んでください
  • 取得したエラーコードはローカル変数に即座に保存し、以降はその変数を使ってください
  • 別APIを挟んでから読むと、元の失敗理由ではなく、その後のAPIの結果を読んでしまうことがあります
  • CreateMutexのように「成功 + エラーコード」のパターンがあるAPIは、ドキュメントで個別に確認してください
  • デバッガのウォッチウィンドウで @err,hrを使えば、コードを書かずにエラーコードを確認できます

保存したコードが2か3(ファイルやパスが見つからない)なら、パスのどこから先が無いのかを「GetLastError 3:コード2との違いと確認順」の順で確かめます。CreateFileが5で失敗したときは「GetLastError 5(ERROR_ACCESS_DENIED)の確認箇所」、6は「GetLastError 6(ERROR_INVALID_HANDLE)の原因と切り分け」、32は共有モードのどちらを直すかを「GetLastError 32「別のプロセスが使用中」」で見ます。FormatMessageに渡しても文章が空のまま返るコードは、メッセージに%1を含んでいることがあり、その扱いは「GetLastErrorの数値をFormatMessageで文章にする」にあります。

目次