7.1 Возвращаемся к Point

Предыдущая  Содержание  Следующая V*D*V

Мы хотим спроектировать препроцессор ooc, который поможет поддерживать наши классы и стандарты кодирования. Лучший способ для разработки такого препроцессора это взять типичный, существующий пример класса и посмотреть как можно уменьшить затраты сил на реализацию, используя разумные предположения о том, что может сделать препроцессор. Одним словом, давайте некоторое время "поиграем" в препроцессор.

Point из главы 4 и раздела 6.10 является хорошим примером: это не базовый класс нашей системы, он требует нового метакласса и у него есть несколько типичных метода. С этого момента мы будем использовать курсив и упоминать его как Point, чтобы подчеркнуть, что он служит только в качестве модели того, что наш препроцессор должен обрабатывать.

Начнём с более или менее очевидного описания класса, который мы можем легко понять, и которое не слишком трудно для чтения препроцессором на основе awk:

 

% PointClass: Class Point: Object { // заголовок

    int x;                          // компоненты объекта

    int y;

%                                   // статически скомпонованные

    void move (_self, int dx, int dy);

%-                                  // динамически скомпонованные

    void draw (const _self);

%}

 

Жирный шрифт в этом описании класса указывает элементы, распознаваемые ooc; обычный шрифт используется для элементов, которые препроцессор читает здесь и воспроизводит в другом месте. Комментарии начинаются с // и продолжаются до конца строки; строки могут быть продолжены с помощью обратной косой черты.

Здесь мы опишем новый класс Point как подкласс Object. Эти объекты имеют новые компоненты х и у, оба типа int. Есть статически компонуемый метод move(), который может изменить свой объект, используя другие параметры. Мы также добавили новый динамически компонуемый метод draw(); Поэтому, мы должны начать с нового метакласса PointClass, расширяя мета суперкласс Class. Аргумент объекта draw() является const, то есть он не может быть изменён.

Если новых динамически связанных методов нет, описание ещё проще. В качестве типичного примера рассмотрим Circle:

 

% PointClass Circle: Point { // заголовок

    int rad;                 // компонент объекта

%}                           // статических методов нет

 

Эти простые, строчно-ориентированные описания, содержащие достаточно информации для того, чтобы полностью сгенерировать файлы интерфейса. Вот шаблон, чтобы предположить, как ooc создал бы Point.h:

 

#ifndef Point_h

#define Point_h

 

#include "Object.h"

 

extern const void * Point;

 

для всех методов в %

    void move (void * self, int dx, int dy);

 

если есть новый метакласс

    extern const void * PointClass;

 

    для всех методов в %-

        void draw (const void * self);

 

void initPoint (void);

 

#endif

 

Жирный шрифт отмечает части шаблона общие для всех файлов интерфейса. Обычный шрифт отмечает информацию, которую ooc должен прочитать в описании класса и вставить в файл интерфейса. Списки параметров немного обрабатываются: _self или const _self преобразуются в подходящие указатели; другие параметры могут быть скопированы напрямую.

Части шаблона используются многократно, например, для всех методов с определённой компоновкой или для всех параметров метода. Другие части шаблона зависят от условий таких, как новый определяемый метакласс. Это обозначено курсивом и отступами.

Описание класса также содержит достаточно информации для получения файла представления. Вот шаблон для генерации Point.r:

 

#ifndef Point_r

#define Point_r

 

#include "Object.r"

 

struct Point { const struct Object _;

    для всех компонентов

        int x;

        int y;

};

 

если есть новый метакласс

    struct PointClass { const struct Class _;

        для всех методов в %-

            void (* draw) (const void * self);

};

 

для всех методов в %-

    void super_draw (const void * class, const void * self);

 

#endif

 

Оригинальный файл можно найти в разделе 6.10. Он содержит определения для двух макросов доступа x() и y(). Чтобы ooc мог вставить их в файл представления, мы принимаем соглашение о том, что файл описания класса может содержать дополнительные строки в дополнение к самому описанию класса. Эти строки копируются в файл интерфейса или если им предшествует строки с %prot — в файл представления. prot относится к защищённой информации — такие строки доступны для реализаций класса и его подклассов, но не к приложению, использующему класс.

Описание класса содержит достаточно информации, поэтому ooc сможет также генерировать значительный объём файла реализации. Давайте в качестве примера посмотрим на различные части Point.c:

 

#include "Point.h"                       // подключение

#include "Point.r"

 

Во-первых, файл реализации подключает интерфейс и файлы представлений.

 

                                         // заголовок метода

void move (void * _self, int dx, int dy) {

    для всех параметров                  // импорт объектов

        если параметр это Point

            struct Point * self = _self;

 

    для всех параметров                  // проверка объектов

        если параметр это объект

            assert(_self);

 

    ...                                  // тело метода

 

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

 

                                         // заголовок метода

static void Point_draw (const void * _self) {

    для всех параметров                  // импорт объектов

        если параметр это Point

            const struct Point * self = _self;

 

    ...                                  // тело метода

 

Для динамически компонуемых методов мы также выполняем проверки, создаём заголовки и импортируем объекты. Шаблон может быть немного другим, чтобы учесть тот факт, что селектор уже должен был проверить, что объекты являются теми, кеми они претендуют быть.

Однако, есть несколько проблем. В качестве подкласса Object наш класс Point может перезаписать динамически компонуемый метод, подобный ctor(), который впервые появился в Object. Если ooc создаёт заголовки всех методов, следует прочитать все описания суперклассов спускаясь к корню дерева класса. От имени суперкласса Object в описании класса для Point мы должны быть в состоянии найти файл описания класса для суперкласса. Очевидное решение состоит в сохранении описания для Object в файле с соответствующим именем, например, Object.d.

 

static void * Point_ctor (void * _self, va_list * app) {

    ...

 

Ещё одна проблема связана с тем, что Point_ctor() вызывает селектор суперкласса и, следовательно, не надо импортировать параметры объектов, как делал Point_draw(). Вероятно, хорошая идея, если у нас будет способ каждый раз говорить ooc, хотим ли мы импортировать и проверять объекты.

 

если есть новый метакласс

    для всех методов в %-

        void draw (const void * _self) {    // селектор

            const struct PointClass * class = classOf(_self);

 

            assert(class -> draw);

            class -> draw(self);

        }

                                            // суперкласс селектора

        void super_draw (const void * class, const void * _self) {

            const struct PointClass * superclass = super(class);

 

            assert(_self && superclass -> draw);

            superclass -> draw(self);

        }

 

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

 

если есть новый метакласс

                                            // конструктор метакласса

static void * PointClass_ctor (void * _self, va_list * app) {

    struct PointClass * self =

                        super_ctor(PointClass, _self, app);

    typedef void (* voidf) ();

    voidf selector;

    va_list ap = * app;

 

    while ((selector = va_arg(ap, voidf))) {

        voidf method = va_arg(ap, voidf);

 

        для всех методов в %-

            if (selector == (voidf) draw) {

                * (voidf *) & self -> draw = method;

                continue;

            }

    }

    return self;

}

 

С помощью цикла над определениями метода в описании класса мы можем полностью сгенерировать конструктор метакласса. Однако, так или иначе мы должны указать ooc надлежащий заголовок метода для использования для ctor() — другой проект мог бы принять решение о других соглашениях для конструкторов.

 

const void * Point;                        // описания классов

если есть новый метакласс

const void * PointClass;

 

void initPoint (void)                      // инициализация

{

    если есть новый метакласс

        if (! PointClass)

            PointClass = new(Class, "PointClass",

                            Class, sizeof(struct PointClass),

                            ctor, PointClass_ctor,

                            (void *) 0);

 

        if (! Point)

            Point = new(PointClass, "Point",

                        Object, sizeof(struct Point),

                        для всех перезаписываемых методов

                        ctor, Point_ctor,

                        draw, Point_draw,

                        (void *) 0);

}

 

Функция инициализации зависит от цикла по всем динамически компонуемым методам, перезаписываемым в данной реализации.

 

Предыдущая  Содержание  Следующая