COM 코딩 사례
이 항목에서는 COM 코드를 보다 효과적이고 강력하게 만드는 방법에 대해 설명합니다.
- __uuidof 연산자
- IID_PPV_ARGS 매크로
- SafeRelease 패턴
- COM 스마트 포인터
__uuidof 연산자
프로그램을 빌드할 때 다음과 유사한 링커 오류가 발생할 수 있습니다.
unresolved external symbol "struct _GUID const IID_IDrawable"
이 오류는 GUID 상수가 외부 링크(extern)로 선언되었고 링커가 상수의 정의를 찾을 수 없음을 의미합니다. GUID 상수의 값은 일반적으로 정적 라이브러리 파일에서 내보내집니다. Microsoft Visual C++를 사용하는 경우 __uuidof 연산자를 사용하여 정적 라이브러리를 연결할 필요가 없도록 할 수 있습니다. 이 연산자는 Microsoft 언어 확장입니다. 식에서 GUID 값을 반환합니다. 식은 인터페이스 형식 이름, 클래스 이름 또는 인터페이스 포인터일 수 있습니다. __uuidof사용하여 다음과 같이 Common Item Dialog 개체를 만들 수 있습니다.
IFileOpenDialog *pFileOpen;
hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL, CLSCTX_ALL,
__uuidof(pFileOpen), reinterpret_cast<void**>(&pFileOpen));
컴파일러는 헤더에서 GUID 값을 추출하므로 라이브러리 내보내기가 필요하지 않습니다.
메모
GUID 값은 헤더에 __declspec(uuid( ... ))
선언하여 형식 이름과 연결됩니다. 자세한 내용은 Visual C++ 설명서의 __declspec 설명서를 참조하세요.
IID_PPV_ARGS 매크로
CoCreateInstance 및 QueryInterface 모두 최종 매개 변수를 void** 형식으로 강제 변환해야 합니다. 이렇게 하면 형식이 일치하지 않는 가능성이 생성됩니다. 다음 코드 조각을 고려합니다.
// Wrong!
IFileOpenDialog *pFileOpen;
hr = CoCreateInstance(
__uuidof(FileOpenDialog),
NULL,
CLSCTX_ALL,
__uuidof(IFileDialogCustomize), // The IID does not match the pointer type!
reinterpret_cast<void**>(&pFileOpen) // Coerce to void**.
);
이 코드는 IFileDialogCustomize 인터페이스를 요청하지만 IFileOpenDialog 포인터를 전달합니다. reinterpret_cast 식은 C++ 형식 시스템을 우회하므로 컴파일러가 이 오류를 catch하지 않습니다. 가장 좋은 경우 개체가 요청된 인터페이스를 구현하지 않으면 호출이 실패합니다. 최악의 경우 함수가 성공하고 일치하지 않는 포인터가 있습니다. 즉, 포인터 형식이 메모리의 실제 vtable과 일치하지 않습니다. 당신이 상상할 수 있듯이, 그 시점에서 좋은 일이 일어날 수 없습니다.
메모
vtable(가상 메서드 테이블)는 함수 포인터의 테이블입니다. vtable은 COM이 런타임에 메서드 호출을 구현에 바인딩하는 방법입니다. 우연히 vtable은 대부분의 C++ 컴파일러가 가상 메서드를 구현하는 방법입니다.
IID_PPV_ARGS 매크로는 이 오류 클래스를 방지하는 데 도움이 됩니다. 이 매크로를 사용하려면 다음 코드를 바꿉다.
__uuidof(IFileDialogCustomize), reinterpret_cast<void**>(&pFileOpen)
다음을 수행합니다.
IID_PPV_ARGS(&pFileOpen)
매크로는 인터페이스 식별자에 대한 __uuidof(IFileOpenDialog)
자동으로 삽입하므로 포인터 형식과 일치하도록 보장됩니다. 수정된(및 올바른) 코드는 다음과 같습니다.
// Right.
IFileOpenDialog *pFileOpen;
hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL, CLSCTX_ALL,
IID_PPV_ARGS(&pFileOpen));
QueryInterface동일한 매크로를 사용할 수 있습니다.
IFileDialogCustomize *pCustom;
hr = pFileOpen->QueryInterface(IID_PPV_ARGS(&pCustom));
SafeRelease 패턴
참조 계산은 프로그래밍에서 기본적으로 쉽지만 지루하기 때문에 쉽게 잘못될 수 있는 항목 중 하나입니다. 일반적인 오류는 다음과 같습니다.
- 사용이 완료되면 인터페이스 포인터를 해제하지 못했습니다. 개체가 제거되지 않으므로 이 버그 클래스로 인해 프로그램에서 메모리 및 기타 리소스가 누출됩니다.
- 잘못된 포인터를 사용하여 Release 호출합니다. 예를 들어 개체를 만들지 않은 경우 이 오류가 발생할 수 있습니다. 이 버그 범주로 인해 프로그램이 중단될 수 있습니다.
- Release 호출된 후 인터페이스 포인터 역참조 이 버그로 인해 프로그램이 중단될 수 있습니다. 설상가상으로 프로그램이 나중에 임의로 충돌하여 원래 오류를 추적하기 어려울 수 있습니다.
이러한 버그를 방지하는 한 가지 방법은 포인터를 안전하게 해제하는 함수를 통해 Release 호출하는 것입니다. 다음 코드는 이 작업을 수행하는 함수를 보여줍니다.
template <class T> void SafeRelease(T **ppT)
{
if (*ppT)
{
(*ppT)->Release();
*ppT = NULL;
}
}
이 함수는 COM 인터페이스 포인터를 매개 변수로 사용하고 다음을 수행합니다.
다음은 SafeRelease
사용하는 방법의 예입니다.
void UseSafeRelease()
{
IFileOpenDialog *pFileOpen = NULL;
HRESULT hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL,
CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen));
if (SUCCEEDED(hr))
{
// Use the object.
}
SafeRelease(&pFileOpen);
}
CoCreateInstance 성공하면 SafeRelease
호출하면 포인터가 해제됩니다.
CoCreateInstance 실패하면 pFileOpen NULL 유지됩니다.
SafeRelease
함수는 이를 확인하고 Release대한 호출을 건너뜁니다.
또한 다음과 같이 동일한 포인터에서 SafeRelease
두 번 이상 호출하는 것이 안전합니다.
// Redundant, but OK.
SafeRelease(&pFileOpen);
SafeRelease(&pFileOpen);
COM 스마트 포인터
SafeRelease
함수는 유용하지만 다음 두 가지를 기억해야 합니다.
- 모든 인터페이스 포인터를 NULL초기화합니다.
- 각 포인터가 범위를 벗어나기 전에
SafeRelease
호출합니다.
C++ 프로그래머로서, 당신은 아마 당신이 이러한 것들 중 하나를 기억할 필요가 없다고 생각하고 있습니다. 결국 C++에 생성자와 소멸자가 있는 이유입니다. 기본 인터페이스 포인터를 래핑하고 포인터를 자동으로 초기화하고 해제하는 클래스를 사용하는 것이 좋습니다. 즉, 다음과 같은 것을 원합니다.
// Warning: This example is not complete.
template <class T>
class SmartPointer
{
T* ptr;
public:
SmartPointer(T *p) : ptr(p) { }
~SmartPointer()
{
if (ptr) { ptr->Release(); }
}
};
여기에 표시된 클래스 정의는 불완전하며 표시된 대로 사용할 수 없습니다. 최소한 복사 생성자, 할당 연산자 및 기본 COM 포인터에 액세스하는 방법을 정의해야 합니다. 다행히 Microsoft Visual Studio는 이미 ATL(활성 템플릿 라이브러리)의 일부로 스마트 포인터 클래스를 제공하므로 이 작업을 수행할 필요가 없습니다.
ATL 스마트 포인터 클래스의 이름은 CComPtr . (여기서는 설명하지 않는 CComQIPtr 클래스도 있습니다.) 다음은 열기 대화 상자CComPtr사용하도록 다시 작성된 예제입니다.
#include <windows.h>
#include <shobjidl.h>
#include <atlbase.h> // Contains the declaration of CComPtr.
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR pCmdLine, int nCmdShow)
{
HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED |
COINIT_DISABLE_OLE1DDE);
if (SUCCEEDED(hr))
{
CComPtr<IFileOpenDialog> pFileOpen;
// Create the FileOpenDialog object.
hr = pFileOpen.CoCreateInstance(__uuidof(FileOpenDialog));
if (SUCCEEDED(hr))
{
// Show the Open dialog box.
hr = pFileOpen->Show(NULL);
// Get the file name from the dialog box.
if (SUCCEEDED(hr))
{
CComPtr<IShellItem> pItem;
hr = pFileOpen->GetResult(&pItem);
if (SUCCEEDED(hr))
{
PWSTR pszFilePath;
hr = pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath);
// Display the file name to the user.
if (SUCCEEDED(hr))
{
MessageBox(NULL, pszFilePath, L"File Path", MB_OK);
CoTaskMemFree(pszFilePath);
}
}
// pItem goes out of scope.
}
// pFileOpen goes out of scope.
}
CoUninitialize();
}
return 0;
}
이 코드와 원래 예제의 주요 차이점은 이 버전이 Release명시적으로 호출하지 않는다는 것입니다. CComPtr 인스턴스가 범위를 벗어나면 소멸자가 기본 포인터에서 Release 호출합니다.
CComPtr 클래스 템플릿입니다. 템플릿 인수는 COM 인터페이스 형식입니다. 내부적으로 CComPtr 해당 형식의 포인터를 보유합니다. CComPtr 클래스가 기본 포인터처럼 작동할 수 있도록 연산자>() 및 연산자&() 재정의합니다. 예를 들어 다음 코드는 IFileOpenDialog::show 메서드를 직접 호출하는 것과 같습니다.
hr = pFileOpen->Show(NULL);
CComPtr 일부 기본 매개 변수 값으로 COM CoCreateInstance 함수를 호출하는 CComPtr::CoCreateInstance 메서드도 정의합니다. 다음 예제와 같이 유일한 필수 매개 변수는 클래스 식별자입니다.
hr = pFileOpen.CoCreateInstance(__uuidof(FileOpenDialog));
CComPtr::CoCreateInstance 메서드는 순전히 편의를 위해 제공됩니다. 원하는 경우 COM CoCreateInstance 함수를 계속 호출할 수 있습니다.
다음