COM에서 오류 처리(Win32 및 C++시작)
COM은 HRESULT 값을 사용하여 메서드 또는 함수 호출의 성공 또는 실패를 나타냅니다. 다양한 SDK 헤더는 다양한 HRESULT 상수를 정의합니다. 시스템 전체 코드의 일반적인 집합은 WinError.h에 정의되어 있습니다. 다음 표에서는 이러한 시스템 전체 반환 코드 중 일부를 보여 줍니다.
상수 | 숫자 값 | Description |
---|---|---|
E_ACCESSDENIED | 0x80070005 | 액세스가 거부되었습니다. |
E_FAIL | 0x80004005 | 알 수 없는 오류입니다. |
E_INVALIDARG | 0x80070057 | 매개 변수 값이 잘못되었습니다. |
E_OUTOFMEMORY | 0x8007000E | 메모리가 부족합니다. |
E_POINTER | 0x80004003 | 포인터 값에 대해 NULL이 잘못 전달되었습니다. |
E_UNEXPECTED | 0x8000FFFF | 예기치 않은 조건입니다. |
S_OK | 0x0 | 성공. |
S_FALSE | 0x1 | 성공. |
접두사 "E_"이 있는 모든 상수는 오류 코드입니다. S_OK 및 S_FALSE 상수는 모두 성공 코드입니다. COM 메서드의 99%가 성공하면 S_OK를 반환할 수 있지만 이 사실을 오해하지 마세요. 메서드는 다른 성공 코드를 반환할 수 있으므로 항상 SUCCEEDED 또는 FAILED 매크로를 사용하여 오류를 테스트합니다. 다음 예제 코드는 함수 호출 성공을 테스트하는 잘못된 방법과 올바른 방법을 보여 줍니다.
// Wrong.
HRESULT hr = SomeFunction();
if (hr != S_OK)
{
printf("Error!\n"); // Bad. hr might be another success code.
}
// Right.
HRESULT hr = SomeFunction();
if (FAILED(hr))
{
printf("Error!\n");
}
성공 코드 S_FALSE는 언급할 가치가 있습니다. 일부 메서드는 S_FALSE를 사용하여 실패가 아닌 음의 조건을 나타낼 수 있습니다. 이것은 메서드가 성공했지만 아무런 영향을 주지 않은 "no-op"를 나타낼 수도 있습니다. 예를 들어 CoInitializeEx 함수는 동일한 스레드에서 두 번째로 호출하는 경우 S_FALSE를 반환합니다. 코드에서 S_OK와 S_FALSE를 구분해야 하는 경우 값을 직접 테스트해야 하지만 다음 예제 코드와 같이 FAILED 또는 SUCCEEDED를 사용하여 나머지 사례를 처리해야 합니다.
if (hr == S_FALSE)
{
// Handle special case.
}
else if (SUCCEEDED(hr))
{
// Handle general success case.
}
else
{
// Handle errors.
printf("Error!\n");
}
일부 HRESULT 값은 Windows의 특정 기능 또는 하위 시스템에만 적용됩니다. 예를 들어 Direct2D 그래픽 API는 프로그램이 지원되지 않는 픽셀 형식을 사용했음을 의미하는 D2DERR_UNSUPPORTED_PIXEL_FORMAT 오류 코드를 정의합니다. Windows 설명서에서는 메서드가 반환할 수 있는 특정 오류 코드 목록을 제공하는 경우가 많습니다. 그러나 이러한 목록을 확정적인 것으로 간주해서는 안 됩니다. 메서드는 항상 설명서에 나열되지 않은 HRESULT 값을 반환할 수 있습니다. 다시 한 번 SUCCEEDED 및 FAILED 매크로를 사용합니다. 특정 오류 코드를 테스트하는 경우 기본 사례도 포함합니다.
if (hr == D2DERR_UNSUPPORTED_PIXEL_FORMAT)
{
// Handle the specific case of an unsupported pixel format.
}
else if (FAILED(hr))
{
// Handle other errors.
}
오류 처리 패턴
이 섹션에서는 COM 오류를 구조화된 방식으로 처리하기 위한 몇 가지 패턴을 살펴봅니다. 패턴마다 고유한 장점과 단점이 있습니다. 선택할 때는 어느 정도는 기호가 작용합니다. 기존 프로젝트로 작업하는 경우 특정 스타일을 금지하는 코딩 지침이 이미 있을 수 있습니다. 어떤 패턴을 채택하든 강력한 코드는 다음 규칙을 따릅니다.
- HRESULT를 반환하는 모든 메서드 또는 함수에 대해 계속하기 전에 반환 값을 확인합니다.
- 사용이 끝난 리소스를 해제합니다.
- NULL 포인터와 같은 유효하지 않거나 초기화되지 않은 리소스에 액세스하려고 시도하지 마세요.
- 리소스를 해제한 후에는 사용하지 마세요.
이러한 규칙을 염두에 두었을 때 오류를 처리하기 위한 네 가지 패턴은 다음과 같습니다.
중첩된 if
HRESULT를 반환하는 모든 호출 후에 if 문을 사용하여 성공을 테스트합니다. 그런 다음, if 문의 범위 내에 다음 메서드 호출을 배치합니다. 더 많은 if 문을 필요한만큼 깊이 중첩할 수 있습니다. 이 모듈의 이전 코드 예제에서는 모두 이 패턴을 사용했지만 여기서도 다시 사용합니다.
HRESULT ShowDialog()
{
IFileOpenDialog *pFileOpen;
HRESULT hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL,
CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen));
if (SUCCEEDED(hr))
{
hr = pFileOpen->Show(NULL);
if (SUCCEEDED(hr))
{
IShellItem *pItem;
hr = pFileOpen->GetResult(&pItem);
if (SUCCEEDED(hr))
{
// Use pItem (not shown).
pItem->Release();
}
}
pFileOpen->Release();
}
return hr;
}
장점
- 변수는 최소한의 범위로 선언할 수 있습니다. 예를 들어 pItem은 사용될 때까지 선언되지 않습니다.
- 각 if 문 내에서 특정 고정 항목은 true입니다. 모든 이전 호출이 성공했으며 획득한 모든 리소스는 여전히 유효합니다. 이전 예제에서 프로그램이 가장 안쪽에 있는 if 문에 도달하면 pItem과 pFileOpen이 모두 유효한 것으로 알려집니다.
- 언제 인터페이스 포인터 및 기타 리소스를 해제해야 하는지는 분명합니다. 리소스를 획득한 호출 바로 뒤에 있는 if 문의 끝에서 리소스를 해제합니다.
단점
- 사용자에 따라 중첩이 너무 깊어서 읽지 못할 수 있습니다.
- 오류 처리는 다른 분기 및 루프 문과 혼합됩니다. 이로 인해 전체 프로그램 논리를 따르기 어려울 수 있습니다.
계단식 if
각 메서드 호출 후 if 문을 사용하여 성공을 테스트합니다. 메서드가 성공하면 if 블록 내에 다음 메서드 호출을 배치합니다. 그러나 if 문을 더 중첩하는 대신, 이전 if 블록 뒤에 각 후속 SUCCEEDED 테스트를 배치합니다. 메서드가 실패하면 함수의 맨 아래에 도달할 때까지 나머지 모든 SUCCEEDED 테스트가 실패합니다.
HRESULT ShowDialog()
{
IFileOpenDialog *pFileOpen = NULL;
IShellItem *pItem = NULL;
HRESULT hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL,
CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen));
if (SUCCEEDED(hr))
{
hr = pFileOpen->Show(NULL);
}
if (SUCCEEDED(hr))
{
hr = pFileOpen->GetResult(&pItem);
}
if (SUCCEEDED(hr))
{
// Use pItem (not shown).
}
// Clean up.
SafeRelease(&pItem);
SafeRelease(&pFileOpen);
return hr;
}
이 패턴에서는 함수의 맨 끝에 리소스를 해제합니다. 오류가 발생하면 함수가 종료될 때 일부 포인터가 유효하지 않을 수 있습니다. 잘못된 포인터에서 Release를 호출하면 프로그램이 충돌하므로(또는 더 나쁜 상황 발생) NULL에 대한 모든 포인터를 초기화하고 해제하기 전에 NULL인지 확인해야 합니다. 이 예제에서는 SafeRelease
함수를 사용합니다. 스마트 포인터를 사용하는 것도 좋습니다.
이 패턴을 사용하는 경우 루프 구문에 주의해야 합니다. 루프 내에서 호출이 실패하는 경우 루프에서 중단합니다.
장점
- 이 패턴은 "중첩된 if" 패턴보다 더 적은 중첩을 만듭니다.
- 전반적인 제어 흐름을 파악하는 것은 더 쉽습니다.
- 리소스는 코드의 한 지점에서 해제됩니다.
단점
- 모든 변수를 선언하고 함수 맨 위에서 초기화해야 합니다.
- 호출이 실패하면 함수가 즉시 종료되지 않고, 불필요한 여러 오류 검사를 수행합니다.
- 제어 흐름은 실패 후 함수를 따라 계속 진행되므로 함수 본문 전체에서 잘못된 리소스에 액세스하지 않도록 주의해야 합니다.
- 루프 내의 오류에는 특별한 사례가 필요합니다.
실패 시 점프
각 메서드 호출 후 실패(성공하지 않음)를 테스트합니다. 실패 시 함수의 아래쪽 가까이에 있는 레이블로 이동합니다. 레이블 뒤, 함수를 종료하기 전에 리소스를 해제합니다.
HRESULT ShowDialog()
{
IFileOpenDialog *pFileOpen = NULL;
IShellItem *pItem = NULL;
HRESULT hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL,
CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen));
if (FAILED(hr))
{
goto done;
}
hr = pFileOpen->Show(NULL);
if (FAILED(hr))
{
goto done;
}
hr = pFileOpen->GetResult(&pItem);
if (FAILED(hr))
{
goto done;
}
// Use pItem (not shown).
done:
// Clean up.
SafeRelease(&pItem);
SafeRelease(&pFileOpen);
return hr;
}
장점
- 전체 제어 흐름을 쉽게 파악할 수 있습니다.
- FAILED 확인 후 코드의 모든 지점에서 레이블로 이동되지 않았으면 이전의 모든 호출이 성공한 것입니다.
- 리소스는 코드의 한 곳에서 해제됩니다.
단점
- 모든 변수를 선언하고 함수 맨 위에서 초기화해야 합니다.
- 일부 프로그래머는 코드에서 goto를 사용하는 것을 좋아하지 않습니다. (그러나 이 goto 사용은 고도로 구조화되어 있으므로 코드가 현재 함수 호출을 벗어나지 않습니다.)
- goto 문은 이니셜라이저를 건너뜁니다.
실패 시 throw
레이블로 이동하는 대신 메서드가 실패할 때 예외를 throw할 수 있습니다. 이렇게 하면 예외로부터 안전한 코드를 작성하는 데 익숙한 경우 좀 더 자연스러운 C++ 스타일을 생성할 수 있습니다.
#include <comdef.h> // Declares _com_error
inline void throw_if_fail(HRESULT hr)
{
if (FAILED(hr))
{
throw _com_error(hr);
}
}
void ShowDialog()
{
try
{
CComPtr<IFileOpenDialog> pFileOpen;
throw_if_fail(CoCreateInstance(__uuidof(FileOpenDialog), NULL,
CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen)));
throw_if_fail(pFileOpen->Show(NULL));
CComPtr<IShellItem> pItem;
throw_if_fail(pFileOpen->GetResult(&pItem));
// Use pItem (not shown).
}
catch (_com_error err)
{
// Handle error.
}
}
이 예제에서는 CComPtr 클래스를 사용하여 인터페이스 포인터를 관리합니다. 일반적으로 코드에서 예외를 throw하는 경우 RAII(Resource Acquisition is Initialization) 패턴을 따라야 합니다. 즉, 모든 리소스는 소멸자에서 리소스가 올바르게 해제되도록 보장하는 개체에 의해 관리되어야 합니다. 예외가 throw되면 소멸자 호출이 보장됩니다. 그렇지 않으면 프로그램에서 리소스가 누수될 수 있습니다.
장점
- 예외 처리를 사용하는 기존 코드와 호환됩니다.
- STL(표준 템플릿 라이브러리)과 같이 예외를 throw하는 C++ 라이브러리와 호환됩니다.
단점
- 메모리 또는 파일 핸들과 같은 리소스를 관리하려면 C++ 개체가 필요합니다.
- 예외로부터 안전한 코드를 작성하는 방법을 잘 이해해야 합니다.
다음