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

ОГЛАВЛЕНИЕ

Еще один способ создания делегатов в стиле C# в C++ , используя комбинацию шаблонов и полиморфизм

Введение

Язык C# имеет возможность, называемую делегаты, которые позволяют легко отделить инициатора события от конечного обработчика. По существу, они выполняют такую же роль, что и указатели функций в C и указатели на функции-члены в C++, но они намного более гибкие. В частности, они могут использоваться для указания на любую функцию любого объекта, если она имеет соответствующую сигнатуру.

Эта статья описывает наш подход к обеспечению такой же функциональности путем использования только стандартного C++. Есть много достойных альтернатив, которые можно легко найти через поиск в Google по ключевым словам "C++ делегаты". Нашей задачей было создание синтаксиса, аналогичного используемому  в управляемом C++ и C#.

Предпосылки

Если вы уже знаете о делегатах все, что вам нужно, пропустите этот раздел.

Делегаты – это не новая идея. Продукты Borland Delphi и C++ Builder использовали их с самого начала, чтобы сопровождать Библиотеку визуальных компонентов, хотя они назывались ‘указатели методов’ в Delphi и ‘замыкания’ в Builder (это одинаковые вещи). По существу, замыкание – это указатель объектно-ориентированной функции. Внутри он просто хранит адрес вызываемой функции и адрес объекта, для которого она вызывается (то есть скрытый параметр 'this', передаваемый функции).

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

Сейчас делегаты в стиле C# предоставляют такой же сервис и для языков .NET, но в стандартном C++ их нет. Указатели на члены-функции очень ограничены по сравнению с делегатами, хотя они использовались совместно с макроопределениями в таких библиотеках, как Borland OWL в прошлом, и с Trolltech's Qt сегодня.

С помощью делегатов .NET вы можете даже присоединить несколько обработчиков к одному событию. Они все будут вызваны (последовательно) при вызове делегата. Это значит, что вы можете безопасно присоединить ваш разработчик к событию без разрушения какой-либо другой связи. Мы еще не использовали эту возможность, но осознаем ее потенциал. Однако мы не знаем, может ли эта возможность быть эффективно реализована. Вызов замыканий Borland с одиночным приведением сокращает пару кодов операции, которые помещают 'this' в стек и вызывают функцию, поэтому они эффективны и малозатратны в использовании. [Жаль, что они не входят в стандарт.] Но как только вы начинаете поддерживать динамическую коллекцию целей, все становится намного сложнее. В идеале .NET должен иметь в наличии очень эффективные эффективные делегаты с одиночным приведением, а также реализовывать делегаты с многократным приведением в их – это лучшее, что можно было бы иметь под рукой.

NET различает 'делегаты' и 'события'. Делегат – это улучшенный указатель функции; делегат используется для событий – член класса, которому вы хотите присвоить адреса обработчиков, которые будут вызваны, когда класс вызовет делегат. Мы считаем это разграничение бесполезным – как еще могут использоваться делегаты? Мы иногда используем термины по очереди.

C# и управляемый C++ имеют довольно аккуратный синтаксис для присваивания обработчиков делегату/событию:

mnuPopup->Popup += new System::EventHandler(this, mnuPopup_Popup);

Когда контекстное меню mnuPopup отображается пользователем, оно вызывает его событие/делегат всплывающего меню. Затем будет вызван обработчик, реализованный в форме mnuPopup_Popup.


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

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

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

  • Делегат может вызывать нестатические функции-члены объекта любого класса, включая виртуальные функции.
  • Делегат может вызывать статические функции-члены любого класса и свободные функции.
  • Делегат имеет многократное приведение.
  • Делегат легко использовать (т.е. синтаксис близок к 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().


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

Чтобы испытать код, загрузите его демонстрационную версию. Все содержится в одном файле. Скомпилируйте его и запустите из командной строки. Этот код был разработан в g++ 3.2. Мы бы хотели знать, в каких других компиляторах он будет работать, а в каких нет, поэтому просим сообщать нам.

struct TShapes
{
  virtual void Square(int i)  
    { std::cout << "TShapes::Square:          " << i << std::endl; }  
  void Triangle(int i)        
    { std::cout << "TShapes::Triangle:        " << i << std::endl; }  
};

struct TDerivedShapes : TShapes
{
  virtual void Square(int i)  
    { std::cout << "TDerivedShapes::Square:   " << i << std::endl; } 
};

struct TThings
{
  void Thing1(int i)          
    { std::cout << "TThings::Thing1:          " << i << std::endl; }  
  static void Thing2(int i)   
    { std::cout << "TThings::Thing2:          " << i << std::endl; }  
};

void Free(int i)              
    { std::cout << "Free:                     " << i << std::endl; }

int main()
{
  // Как обычно, typedef облегчает использование шаблонов.
  typedef Event<void, int>  MyEvent;
  MyEvent event;

  TShapes        shapes;
  TDerivedShapes shapes2;
  TThings        things; 

  // Эти элементы заставят шар крутиться.
  event += new MyEvent::T<TShapes>(&shapes, &TShapes::Square);
  event += new MyEvent::T<TShapes>(&shapes, &TShapes::Triangle);

  // Этот элемент показывает, что виртуальные функции обрабатываются правильно.
  event += new MyEvent::T<TShapes>((TShapes*)&shapes2, &TShapes::Square);

  // Этот элемент показывает, что унаследованные функции обрабатываются правильно.
  event += new MyEvent::T<TDerivedShapes>(
    &shapes2, &TDerivedShapes::Triangle);

  // Этот элемент показывает, что объект События
  // может хранить разнородную
  // коллекцию целей.
  event += new MyEvent::T<TThings>(&things, &TThings::Thing1);

  // Этот элемент показывает, что статические функции обрабатываются правильно.
  event += new MyEvent::S(&TThings::Thing2);
 
  // Этот элемент показывает, что свободные функции обрабатываются правильно.
  event += new MyEvent::S(&Free);
  
  // Вызов события с многократным приведением
  std::cout << "<multicast>" << std::endl;
  event(100); 
  std::cout << "</multicast>" << std::endl;
 
  return 0;
}

Вот что вы должны увидеть при запуске программы:

<multicast>
TShapes::Square:          100
TShapes::Triangle:        100
TDerivedShapes::Square:   100
TShapes::Triangle:        100
TThings::Thing1:          100
TThings::Thing2:          100
Free:                     100
</multicast>