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で文章にする」にあります。
