[ <<< | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Источники | >>> ]


© 2001 А.И. Легалов

Как "прикинуться" объектным обобщением

Использование процедурного подхода не позволяет явно "запихнуть" методы в агрегаты или обобщения. Однако практически любой процедурный язык, имеющий развитые абстрактные типы данных, позволяет легко сымитировать объектное обобщение. Именно этот подход используется при трансляции объектно-ориентированных программ в машинный код. Здесь стоит вспомнить компилятор cfront, разработанный Страуструпом [Страуструп2000]. Пример кода, порождаемого подобным компилятором можно подсмотреть в книге Голуба [Голуб]. Подобный прием наверняка использовался многими программистами еще до наступления эры ООП с целью создания более гибкой программы. Правда, я не согласен с высказыванием Страуструпа, что компилятор в данном случае порождает более эффективный код, чем тот, который может написать программист. Как и в случае программирования на ассемблере, имитация ОО стиля в процедурном подходе позволяет написать более эффективную программу, если учитывать при этом специфику решаемой задачи. Но также нет сомнений, что эта эффективность будет достигаться с большими затратами. А отсутствие контроля во время компиляции ведет к тому, что многие ошибки будут выявлены только при выполнении программы.

Почему-то я решил, что мои заметки не будут полными без простенькой процедурной программки в ОО стиле. Поэтому, не уделяя внимания оптимизации, я привожу свой вариант для рассматриваемого примера. Надеюсь, что программа наглядно демонстрирует те затраты, от которых удалось избавиться, перейдя на объектно-ориентированные языки. Небольшой нюанс проявляется в том, что я попытался по максимуму использовать структуры данных, написанные ранее для вариантного обобщения. Таблица виртуальных функций базового класса shape в данном случае выделена в отдельную структуру, что позволяет на ее основе генерировать конкретные таблицы виртуальных функций для вновь создаваемых специализаций. Код моделируемого объектного обобщения выглядит следующим образом:

//----------------------------------------------------------------
// Структура таблицы виртуальных функций базоваого класса
// Содержит указатели на функции, переопределяемые 
// в моделях производных классов.
struct shape_vtbl {
    void (*In)(shape *_this);    // ввод данных из стандартного потока
    void (*Out)(shape *_this);     // вывод данных в стандартный поток
    double (*Area)(shape *_this);  // вычисление площади фигуры
    void (*Destroy)(shape *_this); // удаление обобщенной фигуры
};
//----------------------------------------------------------------
// Структура, обобщающая все имеющиеся фигуры.
// Моделирует абстрактный базовый класс. 
  struct shape {
    shape_vtbl *vtbl; // Указатель на таблицу виртуальных функций
};
//----------------------------------------------------------------

Имитируя наследование, можно построить прямоугольник и треугольник.

//----------------------------------------------------------------
// Функции, инициализирующие таблицу прямоугольника
void In_shape_rectangle(shape* _this); // ввод данных 
void Out_shape_rectangle(shape* _this); // вывод данных 
double Area_shape_rectangle(shape* _this); // вычисление площади
void Destroy_shape_rectangle(shape* _this); // удаление фигуры

//----------------------------------------------------------------
// Таблица виртуальных функций прямоугольника.
// Задается одна для всех прямоугольников. Спрятана в файле
// Инициализируется специально сформированными расширяющими функциями
static shape_vtbl rect_vtbl = {
    &In_shape_rectangle,     // ввод данных из стандартного потока
    &Out_shape_rectangle,    // вывод данных в стандартный поток
    &Area_shape_rectangle,   // вычисление площади фигуры
    &Destroy_shape_rectangle // удаление динамически созданной фигуры
};

//----------------------------------------------------------------
// Структура, моделирующая прямоугольник - наследник фигуры.
struct shape_rectangle {
    shape base;  // База специализированной фигуры
    rectangle r; // Значимая часть. Может быть написана независимо.
};

//----------------------------------------------------------------
// Функции, инициализирующие таблицу треугольника
void In_shape_triangle(shape* _this);     // ввод данных 
void Out_shape_triangle(shape* _this);    // вывод данных 
double Area_shape_triangle(shape* _this); // вычисление площади фигуры
void Destroy_shape_triangle(shape* _this); // удаление фигуры

//----------------------------------------------------------------
// Таблица виртуальных функций треугольника.
// Задается одна для всех треугольников. Спрятана в файле
// Инициализируется специально сформированными расширяющими функциями
static shape_vtbl trian_vtbl = {
    &In_shape_triangle,     // ввод данных из стандартного потока
    &Out_shape_triangle,    // вывод данных в стандартный поток
    &Area_shape_triangle,   // вычисление площади фигуры
    &Destroy_shape_triangle // удаление динамически созданной фигуры
};

//----------------------------------------------------------------
// Структура, моделирующая треугольник - наследник фигуры.
struct shape_triangle {
    shape base;  // База специализированной фигуры
    triangle t; // Значимая часть. Может быть написана независимо.
};

//----------------------------------------------------------------

Таблицы виртуальных функций имитируются посредством соответствующих статических переменных. Это приводит к тому, что существует несколько одинаковых таблиц размещенных в разных единицах компиляции. Но я специально не стал вводить одну глобальную переменную, чтобы не думать о том, в каком файле ее определить, а в каких - объявить. Кстати, такой прием может использоваться и компилятором [Страуструп2000].

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

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

//----------------------------------------------------------------
void Init(rectangle &r, int x, int y);

// Динамическое создание прямоугольника - наследника по двум сторонам
shape_rectangle *Create_shape_rectangle(int x, int y)
{
    shape_rectangle *sr = new shape_rectangle;
    // Привязка к виртуальной таблице
    sr->base.vtbl = &rect_vtbl;
    Init(sr->r, x, y);
    return sr;
}

// Инициализация прямоугольника - наследника по двум сторонам
void Init_shape_rectangle(shape_rectangle &sr, int x, int y)
{
    // Привязка к виртуальной таблице
    sr.base.vtbl = &rect_vtbl;
    Init(sr.r, x, y);
}

//----------------------------------------------------------------
void Init(triangle &t, int a, int b, int c);

// Динамическое создание треугольника - наследника по трем сторонам
shape_triangle *Create_shape_triangle(int a, int b, int c)
{
    shape_triangle *st = new shape_triangle;
    // Привязка к виртуальной таблице
    st->base.vtbl = &trian_vtbl;
    Init(st->t, a, b, c);
    return st;
}

// Инициализация треугольника - наследника по трем сторонам
void Init_shape_triangle(shape_triangle &st, int a, int b, int c)
{
    // Привязка к виртуальной таблице
    st.base.vtbl = &trian_vtbl;
    Init(st.t, a, b, c);
}

//----------------------------------------------------------------

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

Другой вариант

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

//----------------------------------------------------------------
// Структура, обобщающая все имеющиеся фигуры.
// Моделирует абстрактный базовый класс. 
struct shape {
  // Непосредственное размещение указателей на заменямые функции
  // вместо таблицы виртуальных функций
  void (*In)(shape *_this);      // ввод данных 
  void (*Out)(shape *_this);     // вывод данных 
  double (*Area)(shape *_this);  // вычисление площади фигуры
  void (*Destroy)(shape *_this); // удаление обобщенной фигуры
};
//----------------------------------------------------------------

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

//----------------------------------------------------------------
// Структура, моделирующая прямоугольник - наследник фигуры.
struct shape_rectangle {
    shape base;  // База специализированной фигуры
    rectangle r; // Значимая часть. Может быть написана независимо.
};

//----------------------------------------------------------------
// Структура, моделирующая треугольник - наследник фигуры.
struct shape_triangle {
    shape base;  // База специализированной фигуры
    triangle t; // Значимая часть. Может быть написана независимо.
};

//----------------------------------------------------------------

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

//----------------------------------------------------------------
void Init(rectangle &r, int x, int y);

// Функции, инициализирующие таблицу прямоугольника
void In_shape_rectangle(shape* _this);  // ввод данных 
void Out_shape_rectangle(shape* _this); // вывод данных
double Area_shape_rectangle(shape* _this); // вычисление площади фигуры
void Destroy_shape_rectangle(shape* _this); // удаление фигуры

// Динамическое создание прямоугольника - наследника по двум сторонам
shape_rectangle *Create_shape_rectangle(int x, int y)
{
    shape_rectangle *sr = new shape_rectangle;

    sr->base.In = &In_shape_rectangle;
    sr->base.Out = &Out_shape_rectangle;
    sr->base.Area = &Area_shape_rectangle;
    sr->base.Destroy = &Destroy_shape_rectangle;

    Init(sr->r, x, y);
    return sr;
}

// Инициализация прямоугольника - наследника по двум сторонам
void Init_shape_rectangle(shape_rectangle &sr, int x, int y)
{
    sr.base.In = &In_shape_rectangle;
    sr.base.Out = &Out_shape_rectangle;
    sr.base.Area = &Area_shape_rectangle;
    sr.base.Destroy = &Destroy_shape_rectangle;

    Init(sr.r, x, y);
}

//----------------------------------------------------------------
void Init(triangle &t, int a, int b, int c);

// Функции, инициализирующие таблицу треугольника
void In_shape_triangle(shape* _this); // ввод данных 
void Out_shape_triangle(shape* _this); // вывод данных
double Area_shape_triangle(shape* _this); // вычисление площади фигуры
void Destroy_shape_triangle(shape* _this); // удаление фигуры

// Динамическое создание треугольника - наследника по трем сторонам
shape_triangle *Create_shape_triangle(int a, int b, int c)
{
    shape_triangle *st = new shape_triangle;

    st->base.In = &In_shape_triangle;
    st->base.Out = &Out_shape_triangle;
    st->base.Area = &Area_shape_triangle;
    st->base.Destroy = &Destroy_shape_triangle;

    Init(st->t, a, b, c);
    return st;
}

// Инициализация треугольника - наследника по трем сторонам
void Init_shape_triangle(shape_triangle &st, int a, int b, int c)
{
    st.base.In = &In_shape_triangle;
    st.base.Out = &Out_shape_triangle;
    st.base.Area = &Area_shape_triangle;
    st.base.Destroy = &Destroy_shape_triangle;

    Init(st.t, a, b, c);
}

//----------------------------------------------------------------

Этот подход позволяет ускорить выполнение программы, так как отсутствует дополнительное обращение через ссылку к таблице виртуальных функций. Однако эффект достигается за счет расточительного использования памяти, так как каждая структура должна содержать все указатели. Кроме того, усложняются и становятся более медленными функции, выполняющие конструирование, так как теперь необходимо осуществлять привязку обработчиков специализаций для каждого создаваемого объекта, имитирующего производный класс. К сожалению, здесь нельзя воспользоваться статическими полями структуры. Исходные тексты примера размещены в архиве pp_examp1od.zip.


[ <<< | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Источники | >>> ]