• Программирование
  • C++
  • Визуальное моделирование сложных реагирующих систем при помощи диаграмм состояния UML Harel

Имитация делегатов C# в стандартном C++ - Понимание кода

ОГЛАВЛЕНИЕ

Понимание кода

Давайте поговорим о коде. Если вас не интересует, как он работает, пропустите этот раздел и переходите к Использованию кода.

Проект должен удовлетворять следующим ограничениям:

  • Делегат может вызывать нестатические функции-члены объекта любого класса, включая виртуальные функции.
  • Делегат может вызывать статические функции-члены любого класса и свободные функции.
  • Делегат имеет многократное приведение.
  • Делегат легко использовать (т.е. синтаксис близок к C#).

В нашем решении есть 4 части:

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

Базовый класс выглядит примерно так (Return и Arg1 – это типы из внешнего класса):

class Base
{
  public:
    virtual ~Base() { }
    virtual Return operator()(Arg1) = 0;
};

Производный класс для нестатических функций выглядит примерно так (Return и Arg1 – это типы из внешнего класса):

template <typename Class>
class T : public Base
{
  // Сигнатура, применяемая к указателю на член для целевого класса.
  typedef Return (Class::*Func)(Arg1);

  private:
    Class* mThis; // Указатель на объект, с которым связан делегат.
    Func   mFunc; // Адрес функции объекта делегата.

  public:
    T(Class* aThis, Func aFunc) : mThis(aThis), mFunc(aFunc) { }

    virtual Return operator()(Arg1 arg1)
    {
      return (mThis->*mFunc)(arg1);
    }
};

Производный класс для статических и свободных функций выглядит примерно так (Return и Arg1 – это типы из внешнего класса):

class S : public Base
{
  typedef Return (*Func)(Arg1);

  private:
    Func mFunc;

  public:
    S(Func aFunc) : mFunc(aFunc) { }
 
    virtual Return operator()(Arg1 arg1)
    {
      return mFunc(arg1);
    }
};

Внешний класс выглядит примерно так (много подробностей пропущено):

template <typename Return, typename Arg1>
class Event
{
  private:
    std::vector<Base*> mPtrs;
    class Base { ... };
 
  public:
    template <typename Class>
    class T : public Base { ... }; // нестатический
    class S : public Base { ... }; // статический
   
    // Добавляем новую цель (вызываемую) в наш список.
    Event& operator+=(Base* aPtr)
    {
      mPtrs.push_back(aPtr);
      return *this;
    }

    // Вызываем все цели – код будет вести себя непредсказуемо,
    // если вызываемый объект не существует.
    Return operator()(Arg1 arg1)
    {
      // Здесь есть проблемы:
      // 1. Какой результат должно возвращать многократное приведение?
      // На данный момент возвращается последний вызванный элемент.
      // 2. Нам нужно не сохранять временный результат, когда Return не имеет типа.
      typename std::vector<Base*>::iterator end = mPtrs.end();
      for (typename std::vector<Base*>::iterator i = mPtrs.begin();
        i != end; ++i)
      {
        // Вероятно, специализация для Return == void была бы лучше.
        if ((i + 1) == end)
          return (*(*i))(arg1);
        else
          (*(*i))(arg1);
      }
    }
};

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

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

Похоже, что наш класс События был бы наиболее полезен как открытый член любого класса, планирующего представлять событие. Это нарушает инкапсуляцию, но в противном случае синтаксис добавления целей стал бы весьма неудобным. Нам нужно сделать так, чтобы клиенты класса, использующего Событие, не вызывали operator() для него. Возможно, сделать это мог бы простой адаптер – он должен быть открытым членом, передающим вызовы закрытому члену События, но не предоставляющим operator().