Ответ 1
Причина, по которой COM использует void**
с QueryInterface
, несколько особенна. (См. Ниже.)
Как правило, void**
просто означает указатель на void*
, и его можно использовать для выходных параметров, т.е. параметры, указывающие место, где функция может вернуть значение. Ваш комментарий /* [out] */
указывает, что место, на которое указывает ppvInterface
, будет записано.
"Почему параметры с типом указателя используются как параметры?", спросите вы? Помните, что вы можете изменить две вещи с помощью указательной переменной:
- Вы можете изменить сам указатель, чтобы он указывал на другой объект. (
ptr = ...
) - Вы можете изменить заостренный объект. (
*ptr = ...
)
Указатели передаются функции по значению, т.е. функция получает свою собственную локальную копию исходного указателя, который был передан ей. Это означает, что вы можете изменить параметр указателя внутри функции (1), не влияя на исходный указатель, поскольку изменена только локальная копия. Тем не менее, вы можете изменить заостренный объект (2), и это будет видно за пределами функции, потому что копия имеет то же значение, что и исходный указатель и, таким образом, ссылается на один и тот же объект.
Теперь о COM специально:
-
Указатель на интерфейс (указанный
riid
) будет возвращен в переменной, на которую ссылаетсяppvInterface
.QueryInterface
достигает этого через механизм (2), упомянутый выше. -
С
void**
требуется один*
, чтобы разрешить механизм (2); другой*
отражает тот факт, чтоQueryInterface
не возвращает вновь созданный объект (IUnknown
), а уже существующий: во избежание дублирования этого объекта указатель на этот объект (IUnknown*
) возвращается. -
Если вы спрашиваете, почему
ppvInterface
имеет типvoid**
, а неIUnknown**
, что выглядит более разумным по типу безопасности (так как все интерфейсы должны выводиться изIUnknown
), тогда прочитайте следующий аргумент, взятый из книги Essential COM by Don Box, p. 60 (глава Тип Принуждение и IUnknown):
Одна дополнительная тонкость, связанная с
QueryInterface
, относится к ее второму параметру, который имеет типvoid **
. Очень иронично, чтоQueryInterface
, основа системы COM-типа, имеет довольно непрозрачный прототип типа в С++ [...]IPug *pPug = 0; hr = punk->QueryInterface(IID_IPug, (void**)&pPug);
К сожалению, следующее выглядит так же корректно для компилятора С++:
IPug *pPug = 0; hr = punk->QueryInterface(IID_ICat, (void**)&pPug);
Эта более тонкая вариация также правильно компилируется:
IPug *pPug = 0; hr = punk->QueryInterface(IID_ICat, (void**)pPug);
Учитывая, что правила наследования не применяются к указателям, это альтернативное определение
QueryInterface
не устраняет проблему:HRESULT QueryInterface(REFIID riid, IUnknown** ppv);
То же ограничение распространяется и на ссылки на указатели. Следующее альтернативное определение, возможно, более удобно для клиентов:
HRESULT QueryInterface(const IID& riid, void* ppv);
[...] К сожалению, это решение не уменьшает количество ошибок [...] и , устраняя необходимость в литье, удаляет визуальный индикатор того, что безопасность типа С++ может оказаться под угрозой. Учитывая желаемую семантику
QueryInterface
, типы аргументов, выбранных Microsoft, являются разумными, если они не являются безопасными или элегантными. [...]