C++ COM дизайн. Композиция против множественного наследования
Я пытаюсь встроить элемент управления браузера в мое приложение (IWebBrowser2). Мне нужно реализовать IDispatch, IDocHostShowUI, IDocHostUIHandler и т. Д., Чтобы сделать эту работу. Я делаю это в чистом C++/Win32 API. Я не использую ATL, MFC или любой другой фреймворк.
У меня есть основной класс, называемый TWebf, который создает окно Win32 для вставки элемента управления браузера и выполняет все вызовы OLE, необходимые для его работы. Он также используется для управления браузером с помощью таких методов, как Refresh(), Back(), Forward() и т. Д.
Прямо сейчас это реализуется с композицией. В TWebf есть классы, реализующие все различные интерфейсы (IDispatch, IDocHostShowUI...) в качестве членов (выделенных в стек). Первое, что делает TWebf в своем конструкторе, это дает всем этим членам указатель обратно на себя (dispatch.webf = this;
так далее). QueryInterface, AddRef и Release реализованы как вызовы этих методов в TWebf для всех реализаций интерфейса (путем вызова return webf->QueryInterface(riid, ppv);
например)
Мне не нравится эта циклическая зависимость между TWebf и классами, реализующими интерфейсы. TWebf имеет члена TDispatch, который имеет члена TWebf, который имеет...
Таким образом, я думал о решении этого с множественным наследованием вместо этого. Это также упростит QueryInterface, чтобы всегда иметь возможность просто вернуть this
,
UMLish набросок того, что я хочу, будет выглядеть примерно так: (нажмите для увеличения)
http://img267.imageshack.us/img267/9918/lsactivedesktopuml.png
Как видно из uml, я хочу предоставить минимальные реализации всех интерфейсов, поэтому мне нужно только переопределить эти методы в интерфейсах, которые я на самом деле хочу сделать что-то существенное в TWebf.
Возможна ли моя "реализация множественного наследования"? Это хорошая идея? Это лучшее решение?
РЕДАКТИРОВАТЬ:
Для дальнейшего обсуждения, вот текущая реализация QueryInterface в TWebf
HRESULT STDMETHODCALLTYPE TWebf::QueryInterface(REFIID riid, void **ppv)
{
*ppv = NULL;
if (riid == IID_IUnknown) {
*ppv = this;
} else if (riid == IID_IOleClientSite) {
*ppv = &clientsite;
} else if (riid == IID_IOleWindow || riid == IID_IOleInPlaceSite) {
*ppv = &site;
} else if (riid == IID_IOleInPlaceUIWindow || riid == IID_IOleInPlaceFrame) {
*ppv = &frame;
} else if (riid == IID_IDispatch) {
*ppv = &dispatch;
} else if (riid == IID_IDocHostUIHandler) {
*ppv = &uihandler;
}
if (*ppv != NULL) {
AddRef();
return S_OK;
}
return E_NOINTERFACE;
}
РЕДАКТИРОВАТЬ 2:
Я попытался реализовать это только для пары интерфейсов. Наличие наследования TWebf от IUnknown и TOleClientSite, кажется, работает нормально, но когда я добавил TDispatch в список наследования, он перестал работать.
Отдельно от warning C4584: 'TWebf' : base-class 'IUnknown' is already a base-class of 'TDispatch'
предупреждение, что я также получаю ошибки во время выполнения. Ошибка во время выполнения: "Место чтения нарушения доступа 0x00000000"
Ошибка времени выполнения возникает в строке, касающейся IOleClientSite, а не IDispatch по какой-то причине. Я не знаю, почему это происходит, или это действительно связано с множественным наследованием или нет. Есть какие-нибудь подсказки?
РЕДАКТИРОВАТЬ 3:
Плохая реализация QueryInterface, кажется, была причиной исключения во время выполнения. Как правильно заметил Mark Ransom, этот указатель должен быть приведен до того, как он назначен на *ppv, и при запросе IUnknown требуется особая осторожность. Прочитайте Почему именно мне нужен явный прирост при реализации QueryInterface в объекте с множественным наследованием для превосходного объяснения этого.
Почему именно я получил эту конкретную ошибку времени выполнения, я до сих пор не знаю.
3 ответа
Многократное наследование является очень распространенным способом создания COM-интерфейсов, так что да, это возможно.
Однако QueryInterface все еще должен приводить указатель для каждого интерфейса. Одним интересным свойством множественного наследования является то, что указатель может корректироваться для каждого типа класса - указатель на IDispatch не будет иметь того же значения, что и указатель на IDocHostUIHandler, даже если они оба указывают на один и тот же объект. Также убедитесь, что QueryInterface для IUnknown всегда возвращает один и тот же указатель; поскольку все интерфейсы являются производными от IUnknown, вы получите неоднозначное приведение, если попытаетесь просто привести его непосредственно к нему, но это также означает, что вы можете использовать любой интерфейс в качестве IUnknown, просто выберите первый в родительском списке.
Было бы значительно легче оставаться с композицией. MI имеет много подводных камней, таких как виртуальное наследование, и сильно страдает от ремонтопригодности. Если вам нужно передать это составным классам как элемент данных, вы сделали это неправильно. Что вы должны сделать, это передать это в вызовы методов, если им нужен доступ к другим предоставленным методам. Поскольку вы управляете всеми вызовами методов для составного объекта, не должно быть проблем с вставкой дополнительного указателя. Это делает жизнь НАМНОГО, намного проще для технического обслуживания и других операций.
Множественное наследование имеет пару ограничений
Если два интерфейса запрашивают реализацию функции с одним и тем же именем / сигнатурой, невозможно обеспечить два разных поведения с использованием множественного наследования. В некоторых случаях вам нужна такая же реализация, а в других - нет.
В виртуальной таблице вашего класса будет несколько интерфейсов IUnknown, что может добавить дополнительное использование памяти. Они имеют одинаковые реализации, что приятно.