Глава 2
Интерфейс

Не так давно «космический челнок» «Атлантис» состыковался с российской орбитальной станцией «Мир». Американцы установили на свой корабль российский стыковочный узел, подходящий к соответствую щему узлу станции. Только представьте, что Вы могли бы сделать с таким стыковочным узлом! Его можно установить на Вашем доме, и туда мог бы пристыковаться космический корабль. Вы могли бы установить этот узел на старый «Фольксваген», на башню «Космическая игла» в Сиэтле или свой гараж - и «челнок» мог бы стыковаться и с ними. Пока узел не изменился и пока он установлен на корабле, космический аппарат может стыковаться с чем угодно. Имея два узла, Вы могли бы пристыковывать свой «Фольксваген» к дому, а не ставить его в гараж. Не правда ли, забавно!

Стыковочный узел - это пример интерфейса (interface). Интерфейс обеспечивает соединение двух разных объектов. Хотя стыковочные узлы отлично соединяют друг с другом космические корабли, они не слишком годятся для соединения частей компьютерной программы. Для стыковки в компьютерных программах применяют наборы функций. Именно такие наборы и определяют интерфейс между разными частями программы.

Интерфейс DLL - это набор функций, экспортируемых ею. Интерфейс класса C++ - это набор членов данного класса. Однако когда в этой книге я употребляю слово интерфейс, имеется в виду интерфейс СОМ, а не все прочие. Интерфейс СОМ также включает в себя набор функций, которые реализуются компонентами и используются клиентами. Но СОМ дает более точное определение интерфейса. В СОМ интерфейсом является определенная структура в памяти, содержащая массив указателей на функции. Каждый элемент массива содержит адрес функции, реализуемой компонентом. С точки зрения СОМ интерфейсом является упомянутая структура в памяти; все остальное - это детали реализации, которые СОМ не касаются. Мы остановимся на этой структуре в конце главы, после того как посмотрим на ее реализацию с помощью C++.

На C++ интерфейс реализуется с помощью абстрактных базовых классов. Поскольку каждый компонент СОМ может поддерживать сколь угодно много интерфейсов, для реализации компонента с несколькими интерфейсами мы будем использовать множественное наследование абстрактных базовых классов. Техники мы коснемся ниже, в первом примере.

Давайте более подробно рассмотрим роль интерфейсов в СОМ. Затем реализуем один интерфейс. После этого внимательно присмотримся к некоторым интересным деталям интерфейсов. И, наконец, разберем структуру блока памяти, соответствующего интерфейсу в смысле СОМ.

Интерфейсы - это все

В СОМ интерфейсы - это все. Для клиента компонент представляет собой набор интерфейсов. Клиент может взаимодействовать с компонентом СОМ только через интерфейс. Как мы увидим далее, клиент очень мало знает о компоненте в целом. Часто ему даже не известны все интерфейсы, которые тот поддерживает.

На рисунках в гл. 1 были представлены приложения, построенные из компонентов (эти рисунки напоминают левую часть рис. 2-1). Они могут ввести Вас в заблуждение, так как подчеркивают важность компонентов и не обращают внимания на интерфейсы. С точки зрения программиста СОМ, интерфейсы - важная часть любого приложения. Компоненты сами по себе есть просто детали реализации интерфейсов. Правая часть рис. 2-1 более точно отражает значение интерфейсов для программ СОМ.


РИС. 2-1. В СОМ интерфейсы значат больше, чем реализующие их компоненты.

Повторное использование архитектур приложений

Утверждение, что компонент - всего лишь деталь реализации интерфейса, конечно, преувеличение. В конце концов, интерфейс без реализации ничего не сделает. Однако компонент можно удалить и заменить другим; если новый компонент поддерживает те же интерфейсы, что и старый, приложение будет работать по-прежнему. Отдельные компоненты сами по себе не определяют приложения. Приложение определяют интерфейсы между компонентами. Пока интерфейсы неизменны, компоненты могут появляться и исчезать.

Интерфейсы очень похожи на элементы каркаса сборного дома. Каркас задает структуру, без которой крыша и стены не защитят от стихии. Если Вы не трогаете каркас, дом остается структурно тем же самым. Если заменить кирпичные стены на деревянные, изменится внешний вид, но не структура. Аналогично этому, замена компонентов может изменить поведение приложения, но не его архитектуру. Одно из самых больших преимуществ компонентной модели - возможность повторного использования архитектуры приложения. При помощи тщательно разработанных интерфейсов можно создать архитектуры с очень высокой степенью пригодности к повторному использованию. Просто разрешив заменять некоторые ключевые компоненты, мы добиваемся того, что одна и та же архитектура может поддерживать несколько различных приложений.

Однако построение повторно используемой архитектуры - непростая задача. В некотором смысле для этого надо уметь предсказывать будущее, что вряд ли многим по силам. Тем не менее, есть и другие причины для использования интерфейсов.

Другие преимущества интерфейсов СОМ

Сценарий, описанный в начале этой главы, иллюстрирует две причины, по которым стоит использовать интерфейсы. Во-первых, они позволяют Вам подсоединить космический корабль к «Фольксвагену». Нет, шутки в сторону... Во-первых, интерфейсы предохраняют систему от повреждений при модификации. До тех пор, пока применяются одни и те же стыковочные узлы, и космический корабль, и орбитальная станция могут изменяться. Во вторых, интерфейсы позволяют клиенту работать с разными компонента ми единообразно. Эта способность к унифицированной работе с разными компонентами известна как полиморфизм (polymorphism). Космический «челнок» работает с каждым объектом, имеющим стыковочный узел, так, будто это орбитальная станция. Ниже мы обсудим полиморфизм подробнее.

Интерфейс обеспечивает описанные выше преимущества путем инкапсуляции определенного поведения. Стыковочный узел инкапсулирует механику стыковки. Он скрывает детали того, с чем стыкуется корабль. По скольку корабль стыкуется только с другим стыковочным узлом, постольку станцию можно заменить на очень большую вафельницу, что не повлияет на «челнок»- по крайней мере, в смысле стыковки.

Теперь, когда мы слегка затронули теорию интерфейсов, пора отставить ее в сторону и приложить руки к созданию какого-нибудь интерфейса. В конце этой главы мы погрузимся в теорию с головой.

Реализация интерфейса СОМ

Теперь рассмотрим код, реализующий простой интерфейс. В приведенном ниже тексте программы компонент СА использует IX и ГУ для реализации двух интерфейсов.

class IX              // Первый интерфейс
{
 public:
 virtual void Fx1() = 0;
 virtual void Fx2() = 0;
};
class IY              // Второй интерфейс
{
 public:
 virtual void Fy1() = 0;
 virtual void Fy2() = 0;
};
class CA : public IX, // Компонент
	   public IY
{
 public:
  // Реализация абстрактного базового класса IX.
  virtual void Fx1() {cout « "Fx1" « endl;}
  virtual void Fx2() {cout « "Fx2" « endl;}
  // Реализация абстрактного базового класса IY.
  virtual void Fy1() {cout « "Fy1" « endl;}
  virtual void Fy2() {cout « "Fy2" « endl;}
};

IX и IY - это чисто абстрактные базовые классы, которые используются для реализации интерфейсов. Чисто абстрактный базовый класс (pure abstract base class) - это базовый класс, который содержит только чисто виртуальные функции (pure virtual functions). Чисто виртуальная функция - это виртуальяая функция, помеченная "=0" - знаком спецификатора чистоты (pure specifier). Чисто виртуальные функции не реализуются в классах, в которых объявлены. Как видно из приведенного примера, функции IX:Fx1, IX::Fx2, IY:Fy1 и IY::Fy2 только декларируются. Реализуются же они в производном классе. В приведенном фрагменте кода компонент СА наследует двум чисто абстрактным базовым классам - IX и IY-и реализует их чисто виртуальные функции.

Для того, чтобы реализовать функции-члены IX и IY, СА использует множественное наследование. Последнее означает, что класс является производным более чем от одного базового класса. Класс C++ чаще использует единичное наследование, т, е. имеет только один базовый класс. Далее в этой главе мы более подробно поговорим о множественных интерфейсах и множественном наследовании.

Абстрактный базовый класс напоминает канцелярский бланк, а производный класс заполняет этот бланк. Абстрактный базовый класс определяет функции, которые будет предоставлять производный класс, а производные классы реализуют их. Открытое (public) наследование от чисто абстрактного базового класса называется наследованием интерфейса (interface inheritance), так как производный класс наследует лишь описания функций. Абстрактный базовый класс не содержит никакой реализации, которую можно было бы унаследовать.

В этой книге все интерфейсы будут реализованы при помощи чисто абстрактных базовых классов. Поскольку СОМ не зависит от языка, имеется двоичный стандарт того, что в этой модели считается интерфейсом. В последнем разделе данной главы - «Что за интерфейсом» - представлена эта структура. По счастью, многие компиляторы C++ автоматически генерируют в памяти верную структуру, если использовать чисто абстрактные базовые классы.

IX и IY не совсем интерфейсы в смысле СОМ. Чтобы стать настоящими интерфейсами, IX и IY должны наследовать специальному интерфейсу IUnknown. Однако IUnknown - это предмет следующей главы, поэтому я не буду обсуждать его сейчас. До конца данной главы мы будем считать, что IX и IY - это интерфейсы СОМ.

Соглашения о кодировании

В своих программах я использую некоторые соглашения, чтобы отличать интерфейсы от других классов. Все имена интерфейсов начинаются с буквы «I». Так, «IX» следует читать «интерфейс X». Имена классов имеют префикс «С», и «СА» читается как «класс А».

Другое соглашение состоит в том, что вместо определения интерфейса как класса я использую следующее определение из заголовочного файла OBJBASE.H Microsoft Win32 Software Development Kit (SDK):

#define interface struct

Определение использует ключевое слово struct, а не class, поскольку члены структуры автоматически объявляются имеющими общий доступ, так что не требуется ключевого слова public. Это меньше загромождает код. Ниже повторно приводятся примеры интерфейсов, записанные теперь в рамках новых соглашений.


#include            //Для #define interface struct
interface IX
{
 virtual void __stdcall Fx1() = 0;
 virtual void __stdcall Fx2() = 0;
};

interface IY
{
 virtual void __stdcall Fy1() = 0;
 virtual void __stdcall Fy2() = 0;
};

Чтобы показать интерфейс на картинке, я использую прямоугольник с «разъемом для подключения» на одной из сторон. Пример дан на рис. 2-2.


РИС. 2-2. Компонент с двумя интерфейсами.

На такой основе мы и будем рассматривать и реализовывать интерфейсы СОМ на C++. Согласитесь, что это не сложнее обычной азбуки (А, В, C++,...). Законченный пример

Давайте рассмотрим несложную, но законченную реализацию интерсов IX и IY. Для реализации компонентов мы используем простую программу на C++ без динамической компоновки. Динамическую компоновку добавим в гл. 5, а пока гораздо проще обойтись без нее. В листинге 2-1 класс СА реализует компонент, который поддерживает интерфейсы IX и IY. В качестве клиента в этом примере выступает процедура main.

Копия приведенного в книге кода содержится в файле IFACE.CPP на прилагаемом к книге компакт-диске. Чтобы скомпилировать его с помощью Microsoft Visual C++, введите команду

cl iface.cpp
Соглашение о вызове __stdcall (или Pascal)
Возможно, Вы заметили в приведенном выше примере ключевое слово _stdcall. Это расширение языка, специфичное для компилятора Microsoft. (Вряд ли Вы сомневались, что какое-то расширение должно присутствовать.) Любой компилятор, поддерживающий разработку для Win32, поддерживает и это ключевое слово или его синоним. Это верно для компиляторов Borland, Symantec и Watcom. Функция, помеченная как _stdcall, использует соглашение о вызове языка Pascal. Такая функция выбирает параметры из стека перед возвратом в вызывающую процедуру. В соответствии же с обычным соглашением о вызове C/C++ стек очищает вызывающая процедура, а не вызываемая. В большинстве других языков, в том числе Visual Basic, по умолчанию используется это же стандартное соглашение о вызове. Название «стандартное» применяется потому, что оно используется для всех функций Win32 API, за исключением имеющих переменное число аргументов. Для функций с переменным числом аргументов по-прежнему используется соглашение языка С, или _cdecl. Стандартное соглашение о вызовах применяется в Windows потому, что уменьшает размер кода, а первые версии Windows должны были работать на системах с 640 КБ памяти.

Практически для всех функций, предоставляемых интерфейсами СОМ на платформах Microsoft, используется стандартное соглашение о вызовах. Только для функций с переменным числом аргументов применяется соглашение о вызовах С. Предполагается, что и Вы будете следовать этим правилам. Однако это требование не абсолютно. Вы можете использовать и другие соглашения о вызове, но должны их ясно документировать и учитывать, что клиенты, написанные на некоторых языках, могут оказаться не в состоянии использовать Ваши интерфейсы.

Если Вы предпочитаете слово, которое легче запомнить, используйте pascal. Оно определено в WINDEF.H как

#define pascal __stdcall

Если же Вы полагаете, что наличие в Вашем коде слова pascal сделает Вас жалким Ра$саlистом, можете воспользоваться следующим определением из OBJBASE.H:

#define STDMETHODCALLTYPE __stdcall

IFACE.CPP
//
// Iface.cpp
// Компиляция: cl Iface.cpp
//

#include <iostream.h>
#include <objbase.h>   // Определить интерфейс.

void trace(const char* pMsg) {cout « pMsg « endl ;}

// Абстрактные интерфейсы

interface IX

{

virtual void __stdcall Fx1() = 0 ;

virtual void __stdcall Fx2() = 0 ;

} ;

interface IY
{  

virtual void __stdcall Fy1() = 0 ;

virtual void __stdcall Fy2() = 0 ;

} ;

// Реализация интерфейса
class CA : public IX,

public IY
{
public:

// Реализация интерфейса IX.

virtual void __stdcall Fx1() {cout « "CA::Fx1" « endl ;}

virtual void __stdcall Fx2() {cout « "CA::Fx2" « endl ;}

// Реализация интерфейса IY.

virtual void __stdcall Fy1() {cout « "CA::Fy1" « endl ;}

virtual void __stdcall Fy2() {cout « "CA::Fy2" « endl ;}

} ;

// Клиент
int main()

{

trасе("Клиент: Создание экземпляра компонента.") ;

CA* pA = new CA ;

// Получить указатель IX.
IX* pIX = pA ;

trасе("Клиент: Использование интерфейса IX.") ;

pIX->Fx1(') ;

pIX->Fx2() ;

// Получить указатель IY.
IY* pIY = pA ;

trасе("Клиент: Использование интерфейса IY.") ;

pIY->Fy1() ;

pIY->Fy2() ;

trace("Клиент: Удаление компонента.") ;

delete pA ; 

return 0 ;

}
Результаты работы этой программы таковы:
Клиент: Создание экземпляра компонента.
Клиент: Использование интерфейса IX.
CA::Fx1
CA::Fx2
Клиент: Использование интерфейса IY.
CA::Fy1
CA::Fy2
Клиент: Удаление компонента.
Как видно из текста, клиент и компонент взаимодействуют через два интерфейса. Последние реализованы с помощью двух базовых классов IХ и IY. Компонент реализуется классом CA которые наследует как IX, так и ГУ. Класс СА реализует функции-члены обоих интерфейсов.

Клиент создает экземпляр компонента. Далее он получает указатели на интерфейсы, поддерживаемые компонентом. Потом клиент использует эти указатели совершенно аналогично указателям на классы С++, поскольку интерфейсы реализованы как чисто абстрактные базовые классы. Ключевыми в этом примере являются следующие моменты:

В этом примере я оставил в инкапсуляции некоторые прорехи, которые мы заделаем в нескольких последующих главах, Но мне хотелось бы обсудить кое-какие проблемы сразу, поскольку они очевидны из листинга 2-1.

Взаимодействие в обход интерфейсов

Помните, как я говорил, что клиент и компонент взаимодействуют только через интерфейс? Клиент из листинга 2-1 не следует этому правилу. Он взаимодействует с компонентом посредством pA - указателя на класс CA, а не на интерфейс. Это может показаться несущественным, но на самом деле очень важно. Использование указателя на CA требует, чтобы клиент знал, как объявлен (обычно в заголовочном файле) класс CA. Объявление класса содержит множество деталей реализации. Изменение этих деталей потребует перекомпиляции клиента. Компоненты должны уметь добавлять и удалять интерфейсы без нарушения работы старых клиентов. Это одна из причин, по которым мы настаиваем, что клиент и компонент должны взаимодействовать только через интерфейс.Вспомните, что интерфейсы основаны на чисто абстрактных базовых классах, с которыми не связана какая-либо реализация.

Конечно, не обязательно изолировать клиент от компонента, если они находятся в одном файле. Однако подобная изоляция необходима, если клиент и компонент подключаются друг к другу динамически, особенно когда у Вас нет исходных текстов. В гл. 3 мы исправим пример так чтобы в нем не использовался указатель на СА. Клиенту более не потребуется знать, как объявлен класс СА.

Использование указателя на СА - не единственное место, где клиент из предыдущего примера в обход интерфейса взаимодействует с компонентом. Для управления существованием компонента клиент применяет операторы к new и delete. Эти операторы не только не входят ни в один из интерфейсов, но и специфичны для языка C++. В гл. 4 мы рассмотрим, как удалить компонент через интерфейс без помощи специфичного для языка оператора. В гл. 6 и 7 мы рассмотрим гораздо более мощный способ создания компонентов.

Теперь давайте обсудим некоторые более тонкие детали реализации клиента и компонента из предыдущего примера.

Детали реализации

Листинг 2-1 - это стандартная программа на C++. В ней нет ничего необычного, за исключением того, что она стала нашим первым шагом в создании компонента и клиента СОМ. Очень легко спутать требования СОМ к компоненту и конкретный способ реализации. В этом разделе я проясню некоторые места, где часто возникает такая путаница.

Класс - это не компонент

В листинге 2-1 класс СА реализует один компонент. СОМ не требует, чтобы один класс C++ соответствовал одному компоненту. Вы можете реализовать один компонент при помощи нескольких классов. На самом деле компонент можно реализовать вообще без классов. Классы C++ не используются при реализации компонентов СОМ на С, а потому они не обязательно должны использоваться и на C++. Просто компоненты СОМ гораздо легче реализовать через классы, чем строить вручную.

Интерфейсы не всегда наследуются

Класс СА наследует поддерживаемые им интерфейсы. СОМ не требует, чтобы класс, реализующий интерфейсы, наследовал их, поскольку клиент никогда не видит иерархию наследования в компоненте СОМ. Наследование интерфейсов - это в чистом виде деталь реализации. Вместо того, чтобы собирать все интерфейсы в одном классе. Вы можете реализовав их отдельно и использовать указатели на соответствующие классы. Крейг Брокшмидт использует такой метод в своей книге Inside OLE. Мы будем использовать для реализации всех интерфейсов один класс, поскольку этот метод проще и легче для восприятия; он также делает программирование для СОМ на C++ более естественным.

Множественные интерфейсы и множественное наследование

Компоненты могут поддерживать сколь угодно много интерфейсов. Для поддержки нескольких интерфейсов мы используем множественное наследование. В листинге 2-1 СА является производным от двух интерфейсов IX и IY, которые он поддерживает. Благодаря поддержке множественных интерфейсов компонент можно рассматривать как набор интерфейсов. Это определяет рекурсивно-вложенную природу компонентной архитектуры(см. рис. 2-3). Интерфейс - это набор функций, компонент - набор интерфейсов, а система - набор компонентов. Некоторые считают интерфейсы эквивалентами функциональных возможностей и при добавлении к компоненту новых интерфейсов говорят о появлении новых возможностей. Я же предпочитаю рассматривать интерфейсы как различные варианты поведения компонента. Набор интерфейсов соответствует набору таких вариантов.

РИС. 2-3. Система компонентов - это набор компонентов, из которых каждый поддерживает набор интерфейсов, из которых каждый содержит набор функций.

Конфликт имен

Поскольку компонент может поддерживать несколько интерфейсов, легко возникает конфликт имен функций этих интерфейсов. Действительно, СОМ нет до этого дела. Интерфейсы СОМ отвечают двоичному стандарту; клиент подключается к интерфейсу не по имени самого интерфейса или его функций. Клиент обращается к функции интерфейса по ее адресу в блоке памяти, представляющем интерфейс. Структура этого блока памяти будет рассмотрена в конце данной главы. Техника изменения имен интерфейсов и функций обсуждается в гл. 8.

Другое решение проблемы конфликта имен - отказ от множественного наследования. Классу, управляющему компонентом, нет необходимости наследовать каждому интерфейсу; он может содержать указатели на другие классы, которые реализуют отдельные интерфейсы.

Конфликт имен интерфейсов встречался бы реже, если бы все разработчики СОМ следовали простому соглашению. Начинайте имена интерфейсов и функций с названия Вашей фирмы или программного продукта. Например, вместо IFly для продукта Xyz, можно было бы использовать IXyzFly.

Теперь, став экспертами в реализации интерфейсов, снова погрузимся в теорию.

Теория интерфейсов, часть II

Перед реализацией интерфейсов я обещал, что позже дам некоторые дополнительные сведения из теории. Я Вас не обманывал. В этом разделе мы рассмотрим три вопроса: неизменность интерфейсов СОМ, полиморфизм и наследование интерфейсов.

Интерфейсы не изменяются

Хотя из листинга 2-1 это не очевидно, интерфейсы нельзя изменять, причем нельзя изменять никогда. Это один из наиболее шокирующих моментов СОМ. После того, как интерфейс опубликован, он обязан быть всегда тем же самым. При модификации компонента следует не изменять существующий интерфейс, а создать и добавить новый. Множественные интерфейсы позволяют компоненту поддерживать новые интерфейсы в дополнение к старым. Таким образом, множественные интерфейсы обеспечивают прочный фундамент, на основе которого клиенты и компоненты могут воспринимать новые версии партнеров. Проблему версий мы затронем в следующей главе.

Полиморфизм

Полиморфизм, как Вы помните, - это способность обрабатывать разные объекты единообразно. Поддержка множественных интерфейсов обеспечивает дополнительные возможности для полиморфизма. Если два разных компонента поддерживают один и тот же интерфейс, для работы с ними клиент может использовать один и тот же код. Таким образом, клиент может работать с разными компонентами полиморфно.

Множественные интерфейсы поощряют полиморфизм. Чем больше интерфейсов поддерживает компонент, тем меньше может быть каждый из них. Маленький интерфейс представляет один вариант поведения, а большой - несколько вариантов. Чем больше вариантов поведения поддерживает данный интерфейс, тем более специфичным для конкретной ситуации он становится. Чем специфичнее интерфейс, тем меньше вероятность его повторного использования другим компонентом. Если же интерфейс не используется повторно, то и работающий с ним клиентский код также нельзя использовать повторно.

Например, что имеет больший потенциал повторного применения - единый интерфейс, представляющий поведение вертолета и описывающий полет, зависание, подъем, вращение, вибрацию, удары и падение, или несколько интерфейсов, реализующих отдельные варианты поведения? Интерфейс, представляющий полет, имеет гораздо больше шансов быть повторно использованным, чем интерфейс «вертолета вообще». Вряд ли что-либо, кроме вертолета, будет вести себя в точности как он; однако есть много аппаратов, которые летают.

Замечательный результат полиморфизма — возможность повторного использования всего приложения. Предположим, что Вы пишете приложение Viewer для просмотра растровых изображений (bitmap). Последние реализуются как компоненты СОМ, поддерживающие интерфейс IDisplay. Viewer взаимодействует с компонентами только через этот интерфейс. Приходит Ваш начальник и говорит, что ему нужно просматривать файлы VRML. Вместо того, чтобы писать новую версию Viewer, Вы должны просто написать компонент СОМ, реализующий интерфейс IDisplay, но отображающий не растровые изображения, а файлы VRML. Конечно, написание программы отображения VRML потребует значительной работы, но не придется переписывать приложение целиком.

Возможность повторного применения целых архитектур не возникает автоматически. Она требует тщательного планирования при разработке интерфейсов, чтобы последние могли поддерживать много разных реализации. Не только интерфейсы должны быть универсальными; и клиент должен использовать интерфейс универсальным образом, который не ограничивает возможности реализации интерфейса. Интерфейсы или приложения, не готовые к появлению новых компонентов, не смогут воспользоваться всеми преимуществами полиморфизма и повторно применять целые готовые конструкции. Кстати, в нашем примере маловероятно, что интерфейс IDisplay окажется достаточно универсальным и гибким для отображения файлов'VRML в дополнение к растровым изображениям, если мы не запланируем это сразу. Одна из самых больших трудностей компонентного программирования — разработка повторно используемых, адаптируемых, гибких и готовых к будущим нововведениям интерфейсов.

Теперь, когда мы знаем, как реализовывать интерфейсы, и примерно представляем, на что они способны, давайте посмотрим, что же представляет собой интерфейс СОМ и почему для его реализации используют абстрактный базовый класс.

Что за интерфейсом

Сквозной сюжет этой главы сводится к тому, что интерфейсы СОМ реализуются на C++ при помощи чисто абстрактных базовых классов. В данном разделе объясняется, почему эти классы подходят для реализации интерфейсов. Мы увидим, что чисто абстрактный базовый класс определяет в памяти как раз ту структуру, которую СОМ требует от интерфейса.

Таблица виртуальных функций

Определяя чисто абстрактный базовый класс, мы фактически определяем структуру некоторого блока памяти. Все реализации чисто абстрактных базовых классов являются блоками памяти однотипной структуры. На рис. 2-4 показана структура памяти для абстрактного базового класса, определяемого следующим кодом:

interface IX
{
virtual void __stdcall Fx1() = 0;
virtual void __stdcall Fx2() = 0;
virtual void __stdcall Fx3() = 0;
virtual void __stdcall Fx4() = 0;
};

Определение чисто абстрактного базового класса просто задает формат данных в памяти. Память для структуры не выделяется до тех пор, пока абстрактный класс не будет реализован в производном классе. Когда производный класс наследует абстрактному базовому классу, он наследует и эту структуру.

РИС. 2-4. Пример структуры блока памяти, определяемой абстрактным базовым классом.

Блок памяти, определяемый чисто абстрактным базовым классом, состоит из двух частей. На рис. 2-4 справа показана таблица виртуальных функций (virtual junction table). Таблица виртуальных функций, или vtbl— это массив указателей на реализации виртуальных функций. На рисунке первый элемент vtbl содержит адрес функции Fx1, реализованной в производном классе. Второй элемент содержит адрес Fx2, и т. д. Слева на рисунке показан указатель на vtbl, или просто указатель vtbl. Указатель на абстрактный базовый класс указывает на указатель vtbl, который, естественно, указывает на таблицу vtbl.

Оказывается, что формат блока памяти для интерфейса СОМ совпадает с форматом блока памяти, который компилятор C++ генерирует для абстрактного базового класса. Это значит, что для определения интерфейсов СОМ можно использовать абстрактные базовые классы. Так, интерфейс IX это и интерфейс, и абстрактный базовый класс. Он является интерфейсом СОМ, поскольку формат его структуры в памяти следует спецификации СОМ. Он является и абстрактным базовым классом, поскольку именно так мы его определили.

Конечно, гладко все выглядит только на бумаге. Компилятор C++ не обязан генерировать для абстрактного базового класса структуру, представленную на рис. 2-4. Ни один стандарт не задает такой формат. Причина отсутствия стандарта очевидна - программы на C++ используют один и тот же компилятор для всех исходных файлов, так что от него требуется совместимость только с самим собой. Однако так сложилось, что большинство компиляторов генерируют именно структуру, показанную на рис. 2-4. Все компиляторы C++ для Windows генерируют формат vtbl, подходящий для СОМ Формат интерфейса может различаться для разных операционных систем. Например, на Маc указатель vtbl указывает на фиктивный указатель, а первый указатель функции располагается во втором элементе таблицы.].

Имеется дополнительное требование, которому должен удовлетворять интерфейс, чтобы он был интерфейсом СОМ. Все интерфейсы СОМ должны наследовать интерфейсу lUnknown, до которого мы доберемся в следующей главе. Это означает, что первые три элемента vtbl одни и те же для всех интерфейсов СОМ. Они содержат адреса реализации трех функций-членов IUnknown. Подробнее об этом рассказывается в гл. 3.

Указатели vtbl и данные экземпляра

Зачем же нужен указатель vtbl? Указатель vtbl еще на ступеньку повышает уровень абстракции в процессе получения указателя на функцию по указателю на базовый класс. Это дает нам Дополнительную свободу в реализации интерфейса.

Компилятор C++ может генерировать код, в котором класс, реализующий абстрактный базовый класс, хранит вместе с указателем vtbl информацию, специфичную для экземпляра. Например, класс СА в представленном ниже коде реализует абстрактный базовый класс IX, определенный в предыдущем разделе.

class СА : public IX
{
public:
// Реализация интерфейса IX
virtual void __stdcall Fx1() {cout « "CA::Fx1"« endl;}
virtual void __stdcall Fx2() {cout « m_Fx2 « endl;}
virtual void __stdcall Fx3() {cout « m_Fx3 « endl;}
virtual void __stdcall Fx4() {cout « m_Fx4 « endl;}
// Конструктор
CA(double d)
: m_Fx2(d*d), m_Fx3(d*d*d), m_Fx4(d*d*d*d)
{}
// Данные экземпляра
double m_Fx2;
double m_Fx3;
double m_Fx4;
};

Таблица vtbl и данные класса СА, сгенерированные компилятором, показаны на рис. 2-5. Обратите внимание, что данные экземпляра потенциально доступны через указатель класса СA: Однако обычно клиент не знает, какие именно данные там хранятся, и потому не может обращаться к ним.

РИС. 2-5. Данные, специфичные для экземпляра, хранятся вместе с указателем vtbl

В то время как классы C++ могут обращаться к данным экземпляра напрямую, компоненты СОМ никогда не смогут добраться до них. В СОМ Вы можете работать с компонентом только через функции, и никогда - через переменные. Это соответствует тому способу, которым мы определяем интерфейсы СОМ. У чисто абстрактных базовых классов есть только чисто виртуальные функции, они не имеют данных экземпляра. Множественные экземпляры

Однако указатель vtbl - это больше, чем просто удобное место для хранения данных экземпляра. Он также позволяет разным экземплярам одного класса использовать одну и ту же vtbl. Если мы создадим два экземпляра СA, то получим два отдельных набора данных экземпляра. Однако эти экземпляры могут совместно использовать одну и ту же vtbl и одну и ту жe реализацию. Например, предположим, что мы создали два объекта СA:

int main()
{
// Создать первый экземпляр СА
СА* рА1 = new CA(1.5);
// Создать второй экземпляр СА
СА* рА2 = new CA(2.75);
}

Эти объекты могут использовать одну и ту же vtbl, элементы которой указывают на одни и те же реализации виртуальных функций-членов. Однако у объектов будут разные данные экземпляра (рис. 2-6).

РИС. 2-6. Несколько экземпляров класса используют одну vtbl.

Компоненты СОМ могут, но не обязаны применять указатель vtbl для совместного использования vtbl. Каждый экземпляр компонента СОМ может иметь свою vtbl.

Разные классы, одинаковые vtbl

По настоящему сила интерфейсов проявляется в том, что классы, производные от данного интерфейса, клиент может рассматривать одинаково. Предположим, что мы реализовали класс СВ, который также является производным от IX:

class СВ : public IX
{
public:
// Реализация интерфейса IX
virtual void __stdcall Fx1() {cout « "CB::Fx1" « endl ;}
virtual void __stdcall Fx2() {cout « "CB::Fx2" « endl ;}
virtual void __stdcall Fx3() {cout « "CB::Fx3" « endl ;}
virtual void __stdcall Fx4() {cout « "CB::Fx4" « endl ;}
};

С помощью указателя на IX клиент может работать как с СА, так и с СВ:

void foo(IX* pIX)
{
pIX->Fx1();
pIX->Fx2();
};

int main()
{

// Создать экземпляр СА.
СА* рА = new CA(1.789);

// Создать экземпляр СВ.
СВ* рВ = new CB;

// Получить указатель IX для СА.
IX* pIX = рА;

foo(pIX);

// Получить указатель IX для СВ.
IX* pIX = рВ;

foo(pIX);

}

В данном примере мы использовали и СА, и CB так, словно они являются интерфейсом IX. Это и есть полиморфизм. На рис. 2-7 показан формат Структур памяти для данного примера. Я не нарисовал данные экземпляров, поскольку нам как СОМ-программистам не важно, что они собой представляют.

РИС. 2-7. Полиморфное использование двух разных классов при помощи общего абстрактного базового класса.

Из рис. 2-7 видно, что два наших класса - СА и СВ - имеют отдельные и различные данные экземпляра, vtbl и реализации. Однако доступ к их vtbl может осуществляться одинаково, поскольку формат обеих таблиц один и тот же. Адрес функции Fx1 находится в первом элементе обеих таблиц. адрес Fx2- во втором, и т. д. Формат таблиц соответствует тому, который генерирует компилятор для абстрактного базового класса. Когда класс реализует абстрактный базовый класс, он обязуется следовать данном формату. То же самое верно для компонентов. Когда компонент возвращает указатель интерфейса IX, он обязан гарантировать, что тот указывает на корректную структуру.

Кирпичики СОМ, резюме

В этой главе мы превратили интерфейс из формальной концепции в конкретную структуру в памяти. Мы увидели, что благодаря инкапсуляции деталей реализации интерфейсы защищают систему компонентов от разрушительного влияния изменений. До тех пор, пока интерфейсы неизменны, клиент или компонент можно спокойно изменять. Благодаря этому старые компоненты можно заменять новыми, не нарушая работу системы. Клиенты также могут работать с компонентами, реализующими один и тот же интерфейс, полиморфно.

Кроме того, мы увидели, как реализовать интерфейс на C++ с помощью чисто абстрактного базового класса. Формат блока памяти, генерируемый компилятором C++ для чисто абстрактного базового класса, совпадает с определяемым СОМ форматом для интерфейса.

R этой главе Вы узнали, что такое интерфейс, как его реализовать и использовать. Однако приведенные в примерах интерфейсы - не настоящие интерфейсы СОМ. СОМ требует, чтобы все интерфейсы поддерживали три функции. Со ссылок на них начинается vtbl интерфейса. В следующей главе мы рассмотрим первую из этих трех функций - Querylnterface. Мне не терпится прочитать эту главу - может, я наконец узнаю, как расстыковать «космический челнок» и мой компьютер!

[Назад] [Содержание] [Вперед]


Смотрите также:



Hosted by uCoz