Рекомендации по программированию COM
В этом разделе описываются способы повышения эффективности и надежности кода COM.
Оператор __uuidof
При сборке программы могут возникнуть ошибки компоновщика, аналогичные следующим:
unresolved external symbol "struct _GUID const IID_IDrawable"
Эта ошибка означает, что константу GUID объявляли с внешней компоновкой (экстерном), а компоновщик не мог найти определение константы. Значение константы GUID обычно экспортируется из статического файла библиотеки. Если вы используете Microsoft Visual C++, вы можете избежать необходимости связать статическую библиотеку с помощью оператора __uuidof . Этот оператор является расширением языка Майкрософт. Он возвращает значение GUID из выражения. Выражение может быть именем типа интерфейса, именем класса или указателем интерфейса. С помощью __uuidof можно создать объект Common Item Dialog следующим образом:
IFileOpenDialog *pFileOpen;
hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL, CLSCTX_ALL,
__uuidof(pFileOpen), reinterpret_cast<void**>(&pFileOpen));
Компилятор извлекает значение GUID из заголовка, поэтому экспорт библиотеки не требуется.
Примечание.
Значение GUID связано с именем типа путем объявления __declspec(uuid( ... ))
в заголовке. Дополнительные сведения см. в документации по __declspec в документации по Visual C++.
Макрос 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++, поэтому компилятор не перехватывает эту ошибку. В лучшем случае, если объект не реализует запрошенный интерфейс, вызов просто завершается ошибкой. В худшем случае функция завершается успешно, и у вас есть несогласованный указатель. Другими словами, тип указателя не соответствует фактической vtable в памяти. Как вы можете себе представить, ничего хорошего может произойти в этот момент.
Примечание.
Vtable (таблица виртуальных методов) — это таблица указателей функций. Vtable — это то, как COM привязывает вызов метода к его реализации во время выполнения. Не случайно, vtables — это то, как большинство компиляторов 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));
Шаблон Сейф Release
Подсчет ссылок является одной из тех вещей в программировании, что в основном легко, но также мучен, что делает его легко получить неправильно. Типовые ошибки включают:
- Не удается освободить указатель интерфейса при его использовании. Этот класс ошибок приведет к утечке памяти и других ресурсов программы, так как объекты не уничтожаются.
- Вызов выпуска с недопустимым указателем. Например, эта ошибка может произойти, если объект никогда не был создан. Эта категория ошибок, вероятно, приведет к сбою программы.
- Отмена ссылки на указатель интерфейса после вызова выпуска. Эта ошибка может привести к сбою программы. Хуже, это может привести к сбою программы в случайное время, что затрудняет отслеживание исходной ошибки.
Одним из способов избежать этих ошибок является вызов выпуска через функцию, которая безопасно освобождает указатель. В следующем коде показана функция, которая делает это:
template <class T> void SafeRelease(T **ppT)
{
if (*ppT)
{
(*ppT)->Release();
*ppT = NULL;
}
}
Эта функция принимает указатель com-интерфейса в качестве параметра и выполняет следующие действия.
- Проверяет, имеет ли указатель значение NULL.
- Вызывает выпуск, если указатель не имеет значения NULL.
- Задает указатель на NULL.
Ниже приведен пример использования 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
проверка для этого и пропускает вызов выпуска.
Также можно вызывать 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;
}
Основное различие между этим кодом и исходным примером заключается в том, что эта версия явно не вызывает выпуск. Когда экземпляр CComPtr выходит из область, деструктор вызывает Release на базовом указателе.
CComPtr — это шаблон класса. Аргумент шаблона — это тип com-интерфейса. Внутри системы CComPtr содержит указатель этого типа. CComPtr переопределяет оператор->() и operator&(), чтобы класс действовал как базовый указатель. Например, следующий код эквивалентен вызову метода IFileOpenDialog::Show напрямую:
hr = pFileOpen->Show(NULL);
CComPtr также определяет метод CComPtr::CoCreateInstance, который вызывает функцию COM CoCreateInstance с некоторыми значениями параметров по умолчанию. Единственным обязательным параметром является идентификатор класса, как показано в следующем примере:
hr = pFileOpen.CoCreateInstance(__uuidof(FileOpenDialog));
Метод CComPtr::CoCreateInstance предоставляется исключительно в качестве удобства. Вы по-прежнему можете вызвать функцию COM CoCreateInstance, если вы предпочитаете.
Следующий