"Сущность технологии СОМ. Библиотека программиста" - читать интересную книгу автора (Бокс Дональд)Абстрактные базы как двоичные интерфейсыОказывается, применение техники разделения интерфейса и реализации может решить и проблемы совместимости транслятора/компоновщика C++. При этом, однако, определение класса интерфейса должно принять несколько иную форму. Как отмечалось ранее, проблемы совместимости возникают из-за того, что разные трансляторы имеют различные соображения по поводу того, как 1. передавать особенности языка на этапе выполнения; 2. символические имена будут представлены на этапе компоновки. Если бы кто-нибудь придумал, как скрыть детали реализации транслятора/компоновщика за каким-либо двоичным интерфейсом, это сделало бы написанные на C++ библиотеки DLL значительно более широко используемыми. Двоичная защита, то есть тот факт, что класс интерфейса C++ не использует языковых конструкций, зависящих от транслятора, решает проблему зависимости от транслятора/компоновщика. Чтобы сделать эту независимость более полной, необходимо в первую очередь определить те аспекты языка, которые имеют одинаковую реализацию в разных трансляторах. Конечно, представление на этапе выполнения таких сложных типов, как С-структуры (structs), может быть выдержано инвариантным по отношению к трансляторам. Это – основное, что должен делать системный интерфейс, основанный на С, и иногда это достигается применением условно транслируемых определений типа прагм (pragmas) или других директив транслятора. Второе, что следует сделать, – это заставить все компиляторы проходить параметры функций в одном и том же порядке (слева направо, справа налево) и зачищать стек также одинаково. Подобно совместимости структур, это также решаемая задача, и для унификации работы со стеком часто используются условные директивы транслятора. В качестве примера можно привести макросы WINBASEAPI void WINAPI Sleep(DWORD dwMsecs); Каждый разработчик транслятора определяет эти символы препроцессора для создания гибких стековых фреймов. Хотя в среде производителей может возникнуть желание использовать аналогичную методику для определений всех методов, фрагменты программ в этой главе для большей наглядности ее не используют. Третье требование к независимости трансляторов – наиболее уязвимое для критики из всех, так как оно делает возможным определение двоичного интерфейса: все трансляторы C++ с заданной платформой одинаково осуществляют механизм вызова виртуальных функций. Действительно, это требование единообразия применимо только к классам, не имеющим элементов данных, а имеющим не более одного базового класса, который также не имеет элементов данных. Вот что означает это требование для следующего простого определения класса: class calculator { public: virtual void add1(short x); virtual void add2(short x, short y); }; Все трансляторы с данной платформой должны создать эквивалентные последовательности машинного кода для следующего фрагмента программы пользователя: extern calculator *pcalc; pcalc-gt;add1(1); pcalc-gt;add2(1, 2); Отметим, что требуется не Впрочем, это не такое уж блестящее решение проблемы, как может показаться. Реализация виртуальных функций на C++ на этапе выполнения выливается в создание конструкций Фактически каждый действующий в настоящее время качественный транслятор C++ использует базовые концепции Основываясь на столь далеко идущих допущениях, теперь можно решить проблему зависимости от транслятора. Предполагая, что все трансляторы на данной платформе одинаково реализуют механизм вызова виртуальной функции, можно определить класс интерфейса C++ так, чтобы глобальные операции над типами данных определялись в нем как виртуальные функции; тогда можно быть уверенным, что все трансляторы будут генерировать эквивалентный машинный код для вызова методов со стороны клиента. Это предположение об единообразии означает, что ни один класс интерфейса не имеет элементов данных и ни один класс интерфейса не может быть прямым потомком более чем одного класса интерфейса. Поскольку в классе интерфейса нет элементов данных, эти методы практически невозможно использовать. Чтобы подчеркнуть это обстоятельство, полезно определить члены интерфейса как простые виртуальные функции, указав, что класс интерфейса задает только возможность вызова методов, а не их реализацию. // ifaststring.h class IFastString { public: virtual int Length(void) const = 0; virtual int Find(const char *psz) const = 0; }; Определение этих методов как чисто виртуальных также дает знать транслятору, что от класса интерфейса не требуется никакой реализации этих методов. Когда транслятор генерирует таблицу vtbl для класса интерфейса, входная точка для каждой простой виртуальной функции является или нулевой (null), или точкой входа в С-процедуру этапа выполнения (_purecall в Microsoft C++), которая при вызове генерирует логическое утверждение. Если бы метод не был определен как чисто виртуальный, транслятор попытался бы включить в соответствующую входную точку vtbl системную реализацию метода класса интерфейса, которая в действительности не существует. Это вызвало бы ошибку компоновки. Определенный таким образом класс интерфейса является абстрактным базовым классом. Соответствующий класс реализации должен порождаться классом интерфейса и перекрывать все чисто виртуальные фyнкции содержательными реализациями. Эта наследственная связь проявится в объектах, которые в качестве своего представления имеют двоичное надмножество представления класса интерфейса (которое как раз и есть vptr/vtbl). Дело в том, что отношение «является» («is-a») между порождаемым и базовым классами применяется на двоичном уровне в C++ так же, как и на уровне моделирования в объектно-ориентированной разработке: class FastString : public IFastString { const int m_cch; // count of characters // число символов char *m_psz; public: FastString(const char *psz); ~FastString(void); int Length(void) const; // returns # of characters // возвращает число символов int Find(const char *psz) const; // returns offset // возвращает смещение }; Поскольку Даже несмотря на то, что открытые операторы над типами данных подняты до уровня чисто виртуальных функций в классе интерфейса, клиент не может приписывать значения объектам // ifaststring.h class IFastString { public: virtual int Length(void) const = 0; virtual int Find(const char *psz) const = 0; }; extern "C" IFastString *CreateFastString(const char *psz); // faststring.cpp (part of DLL) // faststring.cpp (часть DLL) IFastString *CreateFastString (const char *psz) { return new FastString(psz); } Как было в случае класса-дескриптора, новый оператор вызывается исключительно внутри DLL Последнее препятствие, которое предстоит преодолеть, относится к уничтожению объекта. Следующая клиентская программа пройдет трансляцию, но результаты будут непредсказуемыми: int f(void) { IFastString *pfs = CreateFastString(«Deface me»); int n = pfs-gt;Find(«ace me»); delete pfs; return n; } Непредсказуемое поведение вызвано тем фактом, что деструктор класса интерфейса не является виртуальным. Это означает, что вызов оператора Очевидное решение этой проблемы – сделать деструктор виртуальным в классе интерфейса. К сожалению, это нарушит независимость класса интерфейса от транслятора, так как положение виртуального деструктора в таблице // ifaststring.h class IFastString { public: virtual void Delete(void) = 0; virtual int Length(void) const = 0; virtual int Find(const char *psz) const = 0; }; extern "C" IFastString *CreateFastString (const char *psz); она влечет за собой соответствующее определение класса реализации: // faststring.h #include «ifaststring.h» class FastString : public IFastString { const int mcch; // count of characters // счетчик символов char *mpsz; public: FastString(const char *psz); ~FastString(void); void Delete(void); // deletes this instance // уничтожает этот экземпляр int Length(void) const; // returns # of characters // возвращает число символов int Find(const char *psz) const; // returns offset // возвращает смещение }; // faststring.cpp #include lt;string.hgt; #include «faststring.h» IFastString* CreateFastString (const char *psz) { return new FastString(psz); } FastString::FastString(const char *psz) : strcpy( } void FastString::Delete(void) { delete this; } FastString::~FastString(void) { delete[] } int FastString::Lengtn(void) const { return } int FastString::Find(const char *psz) const { // O(1) lookup code deleted for clarity // код поиска 0(1) уничтожен для ясности } Рисунок 1.7 показывает представление #include «ifaststring.h» int f(void) { int n = -1; IFastString *pfs = CreateFastString(«Hi Bob!»); if (pfs) { n = pfs-gt;Find(«ob»); pfs-gt;Delete(); } return n; } Отметим, что все, кроме одной, точки входа в DLL |
||||||||
|