Pull to refresh

Простейший делегат на C++

Reading time 7 min
Views 84K
logoВ C# есть делегаты. В python есть делегаты. В javascript есть делегаты. В Java есть выполняющую их роль замыкания. А в C++ делегатов нет O_O. Многие талантливые программисты успешно борются с этим недостатком, разрабатывая и используя sigslots, boost::function и другие ценные и нужные библиотеки. К сожалению, большинство реализаций отличаются не только методом использования, но также эпической сложностью применяемой шаблонной магии. Дабы при изучении исходников boost::function волосы не вставали дыбом, я написал эту небольшую статью, показывающую как самым простым и топорным способом реализовать делегат на C++. Описанная реализация является иллюстративной, имеет множество недостатков и ее вряд ли можно применить в серьезных проектах — зато она максимально простая и позволяет ознакомиться с предметной областью не разбирая трехэтажные шаблоны sigslots :).



Зачем это нужно



Как показывает практика, если большая программа состоит из большого количества маленьких и максимально независимых друг от друга кусочков — тем легче ее развивать, чинить и менять. Современные объектно-ориентированные языки программирования в качестве кусочка предлагают нам объекты — сообственно отсюда и название. Объектом как правило является экземпляр класса, который делает что-то нужно и полезное. Можно всю программу сделать из одного ба-а-а-альшого класса — 'god object antipattern' — и через пару лет внесение любого изменения превратится в проклятье шестого уровня. А можно разбить программу на мелкие, максимально независящие друг от друга классы, и тогда всем будет сухо и комфортно. Но разбив программу таким образом возникает второй вопрос — а как эти классы будут взаимодействовать между собой? Тут на помощь программисту приходят средства декомпозиции, предоставляемые языком программирования. Самое простое средство декомпозиции в C++ — это сделать классы глобальными и непосредственно вызывать их методы. Подход не лишен недостатков — понять кто кого вызывает по прошествии времени становится все труднее, и через пару лет такой подход приведет к тем же последствиям, что и использование единственного класса. Одобренный святой инквизицией вариант — это передача классам указателей на те классы, с которыми им надобно общаться. Причем желательно, чтобы указатели были не просты, а на интерфейсы — тогда менять и развивать программу по прошествии времени станет намного проще.
Тем не менее, интерфейсы не являются серебряной пулей (некоторые говорят, что такой пули, как и ложки — вообще нет). Если объекту нужно всего несколько взаимодействий, например кнопке уведомить о том, что на нее кликнули — то реализация для этих целей отдельного интерфейса займет ощутимое количество строк кода. Также интерфейсы не решают задачу когда одному объекту нужно уведомить о чем-то несколько других — создание и поддержание листа подписки на основании интерфейсов тоже не самое малое число строк кода.
В динамических языках программирования конкуренцию интерфейсам составляют делегаты. Как правило, делегат очень похож на «указатель на функцию» в C++, с той основной разницей что делегат может указывать на метод произвольного объекта. С точки зрения кода использование делегата выглядит обычно так:
delegate example
То же самое с использованием интерфейса иллюстрирует нужность и пользу делегатов:
interface example

Как должен выглядеть делегат на C++?



Это должно быть нечто, что можно ассоциировать с методом произвольного класса а затем вызвать как функцию. В C++ это можно сделать только в виде класса с перегруженным оператором вызова (такие классы обычно называют функторами). В C# для ассоциации делегата и метода используется оператор "+=", но в C++ это к сожалению невозможно — оператор "+=" принимает только один параметр, в то время как указатель на функцию — член класс в C++ определяется двумя параметрами. Следовательно, использование делегата на C++ должно выглядеть примерно так:
delegate usage

Простейшая реализация для одного аргумента



Попробуем реализовать это поведение. Чтобы делегат мог получить указатель на любой метод любого класса его собственный метод Connect() явно должен быть шаблонным. Почему не сделать шаблонным сам делегат? Потому что тогда придется указывать конкретный класс при создании делегата, а это противоречит возможности ассоциировать делегат с любым классом. Также у делегата должен быть перегруженный оператор вызова, тоже шаблонный — чтобы можно было вызвать с теми же типами аргументов, что и у ассоциированного с ним метода. Итак, заготовка делегата будет иметь вид:
delegate skeleton
Метод Connect() можно вызывать с указателем на метод любого класса, а оператор вызова позволяет вызывать сам делегат с любым аргументом. Теперь все что нужно сделать — это каким-то образом сохранить указатель на класс i_class и на метод i_method, чтобы их можно было использовать в операторе вызова. Тут случается затык номер раз — сам делегат о T и M ничего не знает и знать не должен, сохранить их как поля не получится (это аргументы шаблонного метода, который для одного и того же делегата можно вызвать много раз с разными классами и методами). Что делать? Придется обратиться к небольшой шаблонной магии (поверьте, это заклинания первого уровня по сравнению с теми, что применяются в boost:function). Единственный способ в C++ сохранить аргументы шаблона — это создать экземпляр шаблонного класса, который будет параметризирован этими аргументами, и, соответственно, будет их помнить. Следовательно, нам нужен шаблонный класс, который сможет запомнить T и M. А чтобы сохранить указатель на этот класс, он должен наследоваться от интерфейса, не имеющего шаблонных параметров:
delegate with container
В первом приближении этого хватит, чтобы можно было вызвать Connect() для любого метода любого класса и запомнить аргументы. Но запомнить — это половина дела. Вторая половина — это вызвать запомненный метод с переданным нам аргументом. С вызовом есть некая сложность — указатель на метод класса мы сохранили как интерфейс IContainer — как теперь вызвать метод класса параметром произвольного типа, который пользователь передал в operator()?
argument pass problem
Самый простой способ — это запомнить переданный аргумент в контейнере так же как мы делали это для указателя на метод класса, передать контейнер с аргументом «внутрь» m_container, а затем воспользоваться dynamic_cast<>() чтобы «вынуть» аргумент из контейнера. Звучит страшновато, но код достаточно прост:
argument pass solution draft
Последняя проблема на пути к работающему делегату — это извлечение аргумента из контейнера. Для того, чтобы это сделать нужно знать тип аргумента. Но ведь внутри контейнера, хранящего указатель на метод класса, мы не знаем тип аргумента? Тип аргумента не знаем — зато знаем сигнатуру метода, указатель на который мы храним. Следовательно все что нам нужно — это извлечь тип аргумента из сигнатуры. Для этого необходимо воспользоваться трюком с частичной специализацией шаблонов, описанной в моей статье. Выглядеть это будет следующим образом:
argument pass trick
Собственно, все. Получившийся делегат сохраняет указатель на любой метод любого класса и позволяет вызвать его с синтаксисом вызова функции.

Реализация для произвольного количества аргументов



Показанная выше реализация обладает одним недостатком — она работает только с методами у которых ровно один аргумент. А что делать, если метод не принимает аргументов? Или принимает два, а то и три аргумента? Решение, которое применяется для C++ в boost, sigslots и Qt достаточно простое: мы копипастим соответствующие части кода для всех поддерживаемых случаев. Обычно делают поддержку от нуля до четырех аргументов, так как если при связывании двух объектов необходимо передать более четырех аргументов — то у нас что-то не так с архитектурой и наверное мы пытаемся повторить подвиг WinAPI CreateWindow() O_O. Готовый код реализации с поддержкой до двух аргументов и с примером использования представлен ниже. Напоминаю, что он иллюстративный и сильно упрощенный. Отсутствуют многие проверки, имена переменных пожертвованы в пользу компактности и прочее, прочее, прочее. Для production лучше использовать что-нибудь вроде boost::function :)

#include <assert.h>

//  Контейнер для хранения до 2-х аргументов.
struct NIL {};
class IArguments { public: virtual ~IArguments() {} };
template< class T1 = NIL, class T2 = NIL >
  class Arguments : public IArguments
{
  public: Arguments() {}
  public: Arguments( T1 i_arg1 ) :
    arg1( i_arg1 ) {}
  public: Arguments( T1 i_arg1, T2 i_arg2 ) :
    arg1( i_arg1 ), arg2( i_arg2 ) {}
  public: T1 arg1; T2 arg2;
};

//  Контейнер для хранения указателя на метод.
class IContainer { public: virtual void Call( IArguments* ) = 0; };
template< class T, class M > class Container : public IContainer {};

//  Специализация для метода без аргументов.
template< class T >
  class Container< T, void (T::*)(void) > : public IContainer
{
  typedef void (T::*M)(void);
  public: Container( T* c, M m ) : m_class( c ), m_method( m ) {}
  private: T* m_class; M m_method;
  public: void Call( IArguments* i_args )
  {
    (m_class->*m_method)();
  }
};

//  Специализация для метода с одним аргументом.
template< class T, class A1 >
  class Container< T, void (T::*)(A1) > : public IContainer
{
  typedef void (T::*M)(A1);
  typedef Arguments<A1> A;
  public: Container( T* c, M m ) : m_class( c ), m_method( m ) {}
  private: T* m_class; M m_method;
  public: void Call( IArguments* i_args )
  {
    A* a = dynamic_cast< A* >( i_args );
    assert( a );
    if( a ) (m_class->*m_method)( a->arg1 );
  }
};

//  Специализация для метода с двумя аргументами
template< class T, class A1, class A2 >
  class Container< T, void (T::*)(A1,A2) > : public IContainer
{
  typedef void (T::*M)(A1,A2);
  typedef Arguments<A1,A2> A;
  public: Container( T* c, M m ) : m_class( c ), m_method( m ) {}
  private: T* m_class; M m_method;
  public: void Call( IArguments* i_args )
  {
    A* a = dynamic_cast< A* >( i_args );
    assert( a );
    if( a ) (m_class->*m_method)( a->arg1, a->arg2 );
  }
};

//  Собственно делегат.
class Delegate
{
public:

  Delegate() : m_container( 0 ) {}
  ~Delegate() { if( m_container ) delete m_container; }

  template< class T, class U > void Connect( T* i_class, U i_method )
  {
    if( m_container ) delete m_container;
    m_container = new Container< T, U >( i_class, i_method );
  }

  void operator()()
  {
    m_container->Call( & Arguments<>() );
  }

  template< class T1 > void operator()( T1 i_arg1 )
  {
    m_container->Call( & Arguments< T1 >( i_arg1 ) );
  }

  template< class T1, class T2 > void operator()( T1 i_arg1, T2 i_arg2 )
  {
    m_container->Call( & Arguments< T1, T2 >( i_arg1, i_arg2 ) );
  }

private:
  IContainer* m_container;
};

class Victim { public: void Foo() {} void Bar( int ) {} };

int main()
{
  Victim test_class;
  Delegate test_delegate;
  test_delegate.Connect( & test_class, & Victim::Foo );
  test_delegate();
  test_delegate.Connect( & test_class, & Victim::Bar );
  test_delegate( 10 );
  return 0;
}


Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+36
Comments 45
Comments Comments 45

Articles