Сообщений 3    Оценка 336 [+1/-0]         Оценить  
Система Orphus

Мультиметоды и С++

Автор: Клюев Александр
Источник: RSDN Magazine #2-2003
Опубликовано: 19.07.2003
Исправлено: 10.12.2016
Версия текста: 1.0
Введение
Рассуждение
Реализация
Использование
Области применения мультиметодов

Демонстрационный проект MMDemo

Введение

Создатель С++ Бьерн Страуструп в своей книге «Дизайн и эволюция С++» упоминал мультиметоды как одно из перспективных направлений в развитии языка. Однако в настоящее время мультиметоды в С++ не реализованы. В этой статье будет сделана попытка пролить свет на суть мультиметодов, особенности их использования в С++ и возможную реализацию.

Рассуждение

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

      // базовый (абстрактный) класс геометрических объектов
      struct GObject 
{
  virtualvoid draw(Graphics & gfx)  = 0; // виртуальная функция отрисовки
};

// точкаstruct GPoint : GObject 
{
  virtualvoid draw(Graphics & gfx);
};

// отрезокstruct GFin : GObject 
{
  virtualvoid draw(Graphics & gfx);
};

extern GObject * p; // точка или отрезокvoid test(Graphics & gfx) 
{
// вызывается GFin::draw или GPoint::draw
  p->draw(gfx);
};

Классы GPoint и GFin в приведённом примере содержат виртуальный метод draw. Известно, что в каждый нестатический метод класса неявным образом передается указатель на обьект: this. Воспользуемся неким Си-подобным псевдокодом и запишем метод draw следующим образом, обозначив this явно:

      struct GObject { };

struct GPoint : GObject { };

struct GFin : GObject { };

// виртуальные функции записанные псевдокодеvoid draw(virtual GObject * this, Graphics & gfx) = 0;
void draw(virtual GPoint * this, Graphics & gfx);
void draw(virtual GFin * this, Graphics & gfx);

extern GObject * p; // точка или отрезокvoid test() 
{
  // виртуальный вызов функции void draw(virtual GPoint * p) // или void draw(virtual GFin * p)
  draw(p);
}

С помощью псевдокода мы фактически вынесли объявления методов за пределы класса, а ключевое слово virtual перед первым аргументом показывает, что эти функции виртуальные, а не перегруженные, и поиск функции выполняется во время выполнения по vtable первого аргумента this. Давайте так же немного изменим свою точку зрения и скажем, что draw – это глобальная функция с одним виртуальным аргументом. Назовем мультиметодом функцию, в которой несколько виртуальных аргументов. И введем для дальнейшего рассуждения мультиметод join, задачей которого является объединение двух геометрических объектов в фигуру:

      struct Figure;

// мультиметоды для создания Фигуры из двух объектов// абстрактный мультиметод:
Figure * join(virtual GObject * a, virtual GObject * b) = 0;
// создает отрезок через две точки
Figure * join(virtual GPoint  * a, virtual GPoint  * b);
// объединяет точку и отрезок в треугольник
Figure * join(virtual GPoint  * a, virtual GFin    * b);
// соединяет линиями концы отрезков и создает четырехугольник
Figure * join(virtual GFin    * a, virtual GFin    * b);

extern GObject * a; // точка или отрезокextern GObject * b; // точка или отрезокvoid test() {
// вызывается один из трех мультиметодв
  Figure * f = join(a, b);
}

В примере оба аргумента a и b имеют статический тип GObject, но во время выполнения под a и b могут скрываться объекты типов GFin и GPoint, и поиск нужной функции происходит по типам аргументов во время выполнения, т.к. мы обозначили эти аргументы как «виртуальные».

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

      struct GPoint 
{
  virtual Figure* join(virtual GFin *fin);
};

Такая форма записи как бы говорит нам: мультиметод принадлежит классу GPoint и имеет виртуальный аргумент GFin * fin. Но, чтобы не выделять какой-либо класс в качестве владельца мультиметода, будем всегда использовать первый вариант, в котором все классы равны:

Figure * join(virtual GPoint * a, virtual GFin * b);

Еще раз взглянем на прототипы join, чтобы осознать первоначальное определение мультиметодов: виртуальные функции, принадлежащие сразу нескольким классам. Может возникнуть вопрос, почему выбрано именно это определение, а не уже озвученное: глобальные функции с несколькими виртуальными аргументами. Дело в том, что на уровне реализации должна существовать одна общая таблица мультиметодов (mvtable), разделяемая между несколькими классами (в данном случае GObject, GPoint и GFin). Синтаксически же мультиметоды проще объявлять как функции с виртуальными аргументами. Но давайте выберем определение, более близкое к реализации, нежели синтаксису.

Попробуем теперь найти ситуацию, в которой мультиметоды существенно облегчают жизнь программиста. Рассмотрим следующий сценарий (здесь и далее будет подразумеваться, что пользователь работает в некой графической среде): пользователь выделяет два объекта и выбирает операцию «соединить». Понятно, что применить обычную перегрузку мы не можем т.к. тип объектов, выбираемых пользователем, известен только во время выполнения. Зато мы можем воспользоваться мультиметодом join, который и вызовем в функции-обработчике:

      // обработчик
      void join_onClick(Selection * sel) 
{
  Figure * f = join(sel->item_get(0), sel->item_get(1));
}

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

      // создает треугольник из трех точек
Figure * join(virtual GPoint * a, virtual GPoint * b, virtual GPoint * c);
// четырехугольник из отрезка и двух точек
Figure * join(virtual GFin * a, virtual GPoint * b, virtual GPoint * c);

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

      void join_onClick (Selection * sel) 
{
  Figure * f = NULL;
  switch (sel->item_count()) 
  {
  case 2: 
    f = join(sel->item_get(0), sel->item_get(1));
    break;
  case 3:
    f = join(sel->item_get(0), sel->item_get(1), sel->item_get(2));
    break;
  }
}

Что же мы видим? Удобства использования мультиметодов сведены на нет фиксированным списком аргументов. Т.е. для вызова мультиметода было бы удобно использовать прототип:

Figure * join(virtual GObject * argList[], size_t argc);

А для реализации мультиметодов удобны прототипы:

Figure * join(virtual GPoint * a, virtual GPoint * b, virtual GPoint * c);

Figure * join(virtual GPoint * a, virtual GFin * b);

Так как порядок следования аргументов, передаваемых через массив argList[], непредсказуем, будем считать, что и в объявлении мультиметода порядок следования аргументов значения не имеет. Т.е. следующие прототипы эквивалентны:

Figure * join(virtual GPoint * a, virtual GFin * b);

Figure * join(virtual GFin * a, virtual GPoint * b);

В этом случае устраняются все препятствия для использования мультиметодов:

      void join_onClick (GObject * selection[], size_t n) 
{
  // чудесным образом вызывается join(GPoint *, GPoint *, GPoint *),// join(GPoint *, GFin *) или другой
  Figure * f = join(selection, n);
}

Теперь задумаемся над еще одной проблемой из реальной жизни. Обратим внимание на мультиметод:

Figure * join(virtual GFin * a, virtual GFin * b);

Он создает четырехугольник путем соединения концов двух отрезков. Рисунок показывает, что соединить концы можно двумя способами:


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

Figure * join(virtual GFin *, virtual GFin *)
{
  // эта реализация создает обычный четырехугольник// ...
}
Figure * join(virtual GFin *, virtual GFin *)
{
  // а эта – необычный// ...
}

Это выглядит абсурдно, и из-за неоднозначности такой мультиметод невозможно вызвать. Почему? Вспомним, что мы привыкли считать вызов виртуальной функции в С++ атомарной операцией (единой и неделимой), но по сути она состоит из двух частей: поиск по vtable и непосредственно вызов. В данном случае в результате поиска может быть найдено несколько функций, и однозначного вызова быть не может. Поскольку отказываться от удобств нет смысла, откажемся от атомарности и введем следующий сценарий для вызова мультиметодов: поиск функций; принятие решения о вызове; и, наконец, вызов. В реальной жизни это позволило бы создавать программы, работающие по следующему сценарию: пользователь выделяет произвольное число объектов; по этому списку производится поиск методов; программист помещает доступные операции на инструментальную панель; пользователь выбирает среди возможных, нажимает кнопку – и вызывается нужный мультиметод. Чем вам не «программирование будущего»?

Реализация

Теперь попробуем собрать рассуждения в кучу и начнем приближаться к реалиям С++. Итак, мультиметоды – это виртуальные функции, принадлежащие сразу нескольким классам. Что же у них общего? Если просмотреть мультиметоды, приведенные выше, можно сказать, что, во-первых, все аргументы мультиметодов унаследованы от одного общего базового класса GObject. Во-вторых, все приведённые выше мультиметоды относятся к одному виду операции – join. Таким образом, мультиметоды могут быть сгруппированы в семейства по базовому классу аргументов и типу операции:

family Join<GObject> 
{
  // семейство мультиметодов Join для класса GObject// == method declarations =======================
};

Так как семейство создает область видимости, то мультиметодам можно давать более осмысленные имена:

family Join<GObject> {
  Figure * line_from_pp(GPoint * a, GPoint * b);
  Figure * triangle_from_ppp(GPoint * a, GPoint * b, GPoint * c);
  Figure * triangle_from_fp(GFin * a, GPoint * b);
  Figure * box_from_ff(GFin * a, GFin * b);
  Figure * box_x_from_ff(GFin * a, GFin * b);
};

Чтобы осуществилась задуманная стратегия вызова мультиметодов (поиск-решение-вызов), введем следующие «операции» для семейства:

      // первая часть вызова - задаем список аргументов argList
      void Join<GObject>::args_set(GObject * argList[], size_t argc);
// число найденных мультиметодов для заданного argList
size_t Join<GObject>::method_count();
// некая информация об найденном мультиметоде
method_info& Join<GObject>::method_info(size_t index);
// вызов мультиметода - argList разврачивается // в список параметров конкретного метода
Figure * Join<GObject>::invoke(size_t index);

Пример использования:

      // пользователь выделил объекты
      void selection_onChange(GObject * selection[], size_t n) 
{
  // задаем список аргументов для мультиметода
  Join<GObject>::args_set(selection, n);
  // помещаем на тулбар кнопки, соотвествующие разрешенному набору действийfor (size_t i = 0; i < Join<GObject>::method_count(); i++)
    toolbar_button_add(Join<GObject>::method_info(i));
}

// пользователь нажал кнопку на тулбареvoid toolbar_button_onClick(size_t methodIndex) 
{
  // вызываем нужный мультиметод
  Figure * f = Join<GObject>::invoke(methodIndex);
}

Теперь, когда с теоретической точки зрения всё стало ясно, можно подумать и о реализации. Необходимо реализовать поддержку семейств мультиметодов так, чтобы их можно было удобно использовать в С++. Как семейство может быть реализовано физически? Очевидно, что семейству соответствует некая ассоциативная mvtable (multimethod-vtable), ключом для поиска функций в которой является список типов аргументов. В С++ работу по созданию обычной vtable берет на себя компилятор. Чтобы избавить программиста от необходимости создания mvtable вручную, я написал утилиту xmmdc - MultiMethod Description Compiler (см. MMDemo\XMMDC) которая генерирует готовый шаблон mvtable на основе XML-описания.

Описание mvtable в формате XML (MMDemo\MyMVT.xml):

<mvtable name ='MyMVTBase'>

  <method name ='ppp_link' dsc='Make Triangle'>
    <arg type='GPoint'/>
    <arg type='GPoint'/>
    <arg type='GPoint'/>
  </method>

  <method name ='ppp_link' dsc='Make Line'>
    <arg type='GPoint'/>
    <arg type='GPoint'/>
  </method>

  <method name ='pf_link' dsc='Make Triangle'>
    <arg type='GPoint'/>
    <arg type='GFin'/>
  </method>

  <method name ='ffx_link' dsc='Make X-Box'>
    <arg type='GFin'/>
    <arg type='GFin'/>
  </method>

  <method name ='ff_link' dsc='Make Box'>
    <arg type='GFin'/>
    <arg type='GFin'/>
  </method>

</mvtable>

На основе этого описания xmmdc создает заголовочный файл, содержащий реализацию таблицы мультиметодов (MMDemo\MyMVTBase.h):

      #ifndef _MyMVTBase_h_
#define _MyMVTBase_h_
// базовый класс для mvtable (MMDemo\XMMDC\XMVTable.h)#include"XMVTable.h"template <class Owner, class T>
class MyMVTBase : public XMVTable<Owner,T> 
{
// Прототипы мультиметодовpublic:
  
  void ppp_link(GPoint *,GPoint *,GPoint *) {}
  void pp_link(GPoint *,GPoint *) {}
  void pf_link(GPoint *,GFin *) {}
  void ffx_link(GFin *,GFin *) {}
  void ff_link(GFin *,GFin *) {}

// код реализации protected:
  // код для работы мультиметода ppp_linkstruct X_ppp_link : XMethod<Owner,T> 
  {
    enum { argc = 3 };
    
    int * argList_get(bool fc, int &ac) 
    {
      ac = argc;
      staticint lst[] = { GPoint::typeId,GPoint::typeId,GPoint::typeId };
      if(fc)
        std::sort(lst, lst + argc);
      return lst;
    }

    void call(Owner &o, T * objs[]) 
    {
      int i = argc-1;
      T * args[argc];
      std::copy(objs, objs+argc, args);
      o.ppp_link((GPoint *)_arg_pop(args, i, GPoint::typeId),
        (GPoint *)_arg_pop(args, i, GPoint::typeId),
        (GPoint *)_arg_pop(args, i, GPoint::typeId));
    }

    constchar * dsc_get()
    {
      return"Make Triangle";
    }
  } x_ppp_link;

// Остальная часть опущена// ....................
};

#endif// _MyMVTBase_h_

Сгенерированная mvtable поддерживает следующие операции (в подробностях реализации mvtable легко разобраться по исходному тексту):

      // Базовый класс для mvtable. Детали реализации опущены
      template <
  class Owner,  // Класс реализации мультиметодовclass T  // базовый класс аргументов
>
class XMVTable
{
public:
  // задает список аргументовvoid args_set(T * argList [], size_t n);
  // возвращает число найденных мультиметодов для последнего argList
  size_t  method_count() const;
  // получает информацию о найденном мультиметоде
  Method * method_get(int idx); // описание см. ниже// вызывает мультиметод с сохраненным списком аргументов argListvoid  invoke(Method * m);
};

// класс, возвращаемый функцией XMVTable:: method_get// приведены только необходимые пользователю функции template <class Owner, class T>
struct XMethod
{
  // описание мультиметода или ключ// фактически, значение аттрибута dsc из XML файлаvirtualconstchar * dsc_get() = 0;
public:
  // Пользовательские данные. // Программист может связать с мультиметодом дополнительные данные,// используя эту переменную.void * userData;
};

Использование

А нам теперь остается только унаследоваться от этого класса и реализовать нужные методы (см. MMDemo\MyMVT.h):

      #ifndef _MYMVT_h_
#define _MYMVT_h_

#include"GObject.h"#include"MyMVTBase.h"// реализация семейства мультиметодов Join<GObject>class MyMVT : public MyMVTBase<MyMVT,GObject> {
public:

  GFigure    figure;

// overridespublic:

    void ppp_link(GPoint *,GPoint *,GPoint *);
    void pp_link(GPoint *,GPoint *);
    void pf_link(GPoint *,GFin *);
    void ffx_link(GFin *,GFin *);
    void ff_link(GFin *,GFin *);

};

//реализация одного из мультиметодовvoid MyMVT::ppp_link(GPoint * a, GPoint * b, GPoint * c) {
  figure.line_add(a->A, b->A);
  figure.line_add(b->A, c->A);
  figure.line_add(c->A, a->A);
}

#endif// _MYMVT_h_

Чтобы все это работало, к классам аргументов предъявляются определённые требования. Эти классы должны содержать статическую переменную typeId, уникальную в рамках иерархии классов, и виртуальную функцию typeId_get для определения типа во время исполнения.

      struct GObject 
{
  virtualint typeId_get()  = 0;
};

struct GPoint : GObject 
{
  enum { typeId = 1 };
  int typeId_get() 
  {
    return typeId; 
  }
};

struct GFin : GObject
{
  enum { typeId = 2 };
  int typeId_get()
  {
    return typeId;
  }
};

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

      class CMMDemoDlg : public CDialog 
{
  MyMVT _mvt; // таблица мультиметодов в классе диалога// ...
};

// пользователь выбрал объекты
LRESULT CMMDemoDlg::canvas_onSel(WPARAM,LPARAM) 
{
  int i, c = 0;
  GObject * objs[5];
  
  // формируем список аргументовfor (i = 0; i < _canvas.object_count(); i++) 
  {
    GObject * o = _canvas.object_get(i);
    if (o->selected_is())
      objs[c++] = o;
  }
  // передаем список в mvtable
  _mvt.args_set(objs, c);

  _mmList.ResetContent();
  // выдаем пользователю список доступных операцийfor (i = 0; i < _mvt.method_count(); i++) 
  {
    MyMVT::Method * mm = _mvt.method_get(i);
    int idx = _mmList.AddString(mm->dsc_get());
    _mmList.SetItemDataPtr(idx, mm);
  }
  return 0;
}

// пользователь выбрал операциюvoid CMMDemoDlg::invoke_onClick() 
{
  int idx = _mmList.GetCurSel();
  if(LB_ERR == idx)
    return;

  _mvt.figure.pts.clear();
  // вызываем выбранный метод
  MyMVT::Method * mm = (MyMVT::Method *)_mmList.GetItemDataPtr(idx);
  _mvt.invoke(mm);
  // выводим результат
  _canvas.object_add(&_mvt.figure);
}

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

      extern MyMultimethodVTable mvt;

void test(MyObject * argv[], size_t argc, MyNonVirtualArg & arg)
{
  mvt.nonVirtualArg = arg;
  mvt.args_set(argv, argc);
  mvt.invoke(mvt.method_get(0));
}

Работа программы показана на скриншотах:


Рисунок 1. Выбраны две точки, найден мультиметод pp_link.


Рисунок 2. Результат выполнения мультиметода pp_link.


Рисунок 3. Выбраны два отрезка, найдены методы ffx_link и ff_link.


Рисунок 4. Результат ff_link.


Рисунок 5. Результат работы метода ffx_link


Рисунок 6. Выбраны точка и отрезок, найден метод pf_link.

И, в заключение, систематизируем процесс добавления в проект мультиметодов. Процесс должен состоять из следующих шагов:

<mvtable name='ClassAndHeaderName'>
xmmdc test.xml

Области применения мультиметодов

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

      class Hero : public GameObject
{
// ...
};

class Frodo : public Hero
{
// ...
};

class Item : public GameObject
{
// ...
};

class RingOfPower : public Item
{
// ...
};

Behaviour* behaviour_create(virtual Hero *hero, virtual Item *item);
Behaviour* behaviour_create(virtual Frodo *frodo, virtual RingOfPower *ring);

Весьма полезными окажутся мультиметоды для реализации drag-n-drop операций, так как Drag-n-drop операция разделяется между двумя классами (DropTarget и DropObject) и является мультиметодом в чистом виде:

      void DragDrop(virtual SomeView*, virtual SomeItem*);

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

Приведенные примеры показывают, что хотя мультиметоды еще и не освоены массами, но могут существенно упростить решение задач программирования во многих областях.


Эта статья опубликована в журнале RSDN Magazine #2-2003. Информацию о журнале можно найти здесь
    Сообщений 3    Оценка 336 [+1/-0]         Оценить