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

    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;
    }
    


    Support the author
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 45

      +9
      Интересно оформлен код )
        0
        А мне тяжело читалось. В консоли код выглядит неплохо, потому как весь экран темный, а на белом фоне хабры — заставляет напрягать глаза.
          0
          Увы, слои тут использовать нельзя, если использовать тэг <code> то не получится ставить стрелки, подчеркивания, выделения и прочее :(.
            0
            В свое время после полугода работы на шарпе, как-то автоматически заложил в архитектуру приложения делегаты, будучи полностью уверенным что С++ их поддерживает… Это стоило мне 3-ех бессонных ночей… Цеплять Boost показалось излишним, поэтому пришел к подобному вашему коду.
              0
              Хорошая штука делегаты. Особенно их Qt реализация. Правда, в последнее время я все больше ползаю вокруг event bus — ИМХО, если правильно приготовить, дают очень хорошее архитектурное решение среднего слоя для связи компонентов программы.
        0
        У меня какая-то беда с вставлением картинок — в Internet Explorer отображаются пиксел-в-пексел, в chrome с увеличением ~130%. Что-то тут с DPI не то ©. Может тэгу <img> какой атрибут добавить?
          +2
          Может быть в Chrome увеличение на 130% стоит? У меня все нормально, глянь в параметрах.
          +2
          Dropbox — не лучший хостинг картинок для хабра.
            +2
            А что не так? Сколько публикую — все отлично держит. Если будут проблемы с нагрузкой — переложу на хабрасторадж. Дропбокс удобен тем, что можно легко и быстро вносить правки в изображения для статей.
              +1
              Дропбокс держит только если статью никто не читает. :)
                +1
                Только что в этом убедился. Переложил на хабрасторадж :)
            +2
            Писал когда-то свой, остановился в итоге на boost::signal, по крайней мере бардака с кол-вом параметров нет.

            По данному коду могу сказать одно, что не стоит мешать соль с сахаром, а точнее не стоит в один делегат пихать методы с разными сигнатурами, это плохо кончится. Т.к. здесь работа с кол-вом параметров идет в рантайме вызвав делегат с неподходящими параметрами мы словим assert, но assert используется слегка для других целей. Думаю здесь было бы правильнее кидать исключение std::invalid_argument.

            Я бы все же посоветовал строго типизировать и не играть с dynamic_cast, лишнее это. Строгая типизация даст ошибку на этапе компиляции, а данный код даст ошибку в рантайме и это очень плохо.
              +2
              Реализация со строгой типизацией будет чуток потяжелее по коду. Как я уже писал, цель статьи — обрисовать как ЭТО вообще делается. А «промышленную» реализацию смотреть можно уже в тех же boost::signal. Проблема в том, что для неподготовленного читателя boost::signal — это ого-го какая шаблонная магия. А я, соответственно, постарался эту магию расписать на самых простых, вырожденных примерах.
                0
                Ну почему же, если поддержать ограниченное кол-во параметров, то думаю будет проще.
                  0
                  Да нет, можно конечно написать что-нибудь простое вида
                  Delegate2< void, int, int > delegate, но это не проиллюстрирует работу шаблонной магии и для практического применения такая реализация мало полезна, ИМХО.
                    +2
                    В этом плане согласен, магии нет. Магия внутри boost::mpl )
              0
              А мне понравились вот эти www.rsdn.ru/article/cpp/delegates.xml. Очень удобно и легковесно.
                0
                Тоже хорошая реализация, но там префикс используется по количеству агрументов плюс нужно вручую прототип прописывать. Ну и код, понятное дело, сложнее. ИМХО, буст — наше все :).
                  0
                  Автор пишет, что в JavaScript есть делегаты. Это не совсем верно, так как делегат подразумевает четкую типизацию возвращаемого значения и аргументов. Конечно же, в JavaScript это реализовать типизацию аргументов и возвращаемых значений невозможно. Однако, функции являются одним из типов данных, поэтому их можно, например, передавать в другие функции как обычные переменные. Так что механизм делегатов можно повторить, но это не будет делегатами.
                    +1
                    делегат подразумевает четкую типизацию возвращаемого значения и аргументов

                    Википедия с вами не согласится :). В общем случае объект типа «делегат» и объект типа «first-class function object» очень похожи. Разница в деталях — например, делегат может быть связан с более чем одной функцией. Или быть асинхронным. Определение с википедии:

                    «Programming languages in general do not support delegation as a language concept, but there are a few exceptions, most notably ECMAScript»
                      0
                      Определение с википедии :)
                      A delegate is a form of type-safe function pointer
                        0
                        … «used by the .NET Framework»? То, что в C# «делегат» — это термин для обозначения функциональности самого языка не отменяет то, что в более широком смысле делегат обозначает объектно-ориентированный указатель на функцию. В любом случае, ИМХО, какая разница как назвать?
                          0
                          Возможно я придираюсь конечно, но с моей точки зрения, делегаты — это, действительно, понятие, введенное с приходом языка С#, но задолго до этого они существовали во многих функциональных языках, к которым JavaScropt тоже можно отнести
                    +1
                    Спасибо, мощно
                      0
                      Стараюсь. Может, когда-нибудь, смогу так же рассказать про реализацию event bus O_O.
                      +2
                      >А в C++ делегатов нет — издержки компилируемого языка.

                      Это неверно. Компилируемость (или иная стратегия трансляции языка) не влияет на наличие или отсутствие делегатов.
                        0
                        Соглашусь, это меня что-то переклинило. Заменил странное утверждение на странный анимэшный смайлик О_О.
                        +1
                        > Как должен выглядеть делегат на C++?

                        Вот так:

                        Victim test_class;
                        Delegate test_delegate;
                        
                        test_delegate.Connect([&test_class] { test_class.Foo(); });
                        


                        Забудьте вы уже этот С++2003, используйте C++0x! ;)
                          0
                          ИМХО, рановато еще. Вот примут финальную версию стандарта, будет поддержка из коробки (а не спецключами компилятора) в основных компиляторах и IDE — тогда самое то.

                          BTW, написанное замыкание по коду почти столько же места занимает. В чем профит?
                            0
                            Профит в том, что делегатом может быть любая функция без параметров, а не только функция-член какого-то класса. Ну или например функция/функция-член, которой передаются какие-то дополнительные параметры:

                            test_delegate.Connect([&test_class] { test_class.Foo(10, 20, 30, 40); });

                            Да и вообще, можно саму обработку кода прям в внутрь лямбды поместить.
                              0
                              Приведенный мною код довольно легко расширяется для поддержки функций, не являющихся членами класса. Достаточно специализировать Delegate для void (*)(что-то-там), спрятав тип класса.

                              Помещение самой обработки внутрь лямбд — это то, что активно использует Java. Субъективно кода получается больше, чем при использовании делегатов.
                                0
                                Конечно расширяется, кто спорит то? Потом вы его расширите на функциональные объекты (с перегруженным оператором (), в том числе другие делегаты), потом расширите на биндинг параметров, потом сделаете имитацию лямбд с помощью операторов и placeholder'ов, и получитие boost.function + boost.bind + boost.lambda. Какой смысл, если нормальные лямбды уже на подхоже, и даже уже есть в msvs и gcc?
                              +1
                              Уже пора как минимум в отношении того, что 2010 микрософтовский поддерживает из коробки (и без ключей) — лямбды, auto, rvalue-ссылки и std::function с shared_ptr. Они уже не изменятся.
                            0
                            Удивился, что в статье не упомянуты указатели на методы типа double (Foo::*funcPtr)( long ) = &Foo::One;
                              0
                              Мне не сложно, но зачем их упоминать? O_O. Это же базовый синтаксис языка, он в любом учебнике описан. Какой интерес про него в статье писать? :)
                                0
                                Чтобы рассказать, что делегаты лучше указателей из-за типизации. Это не такая уж очевидная вещь.
                                  0
                                  У меня есть «Как правило, делегат очень похож на „указатель на функцию“ в C++, с той основной разницей что делегат может указывать на метод произвольного объекта.» :). Этого должно хватить.
                              0
                              Спасибо за статью!
                              В принципе всё более менее понятно за исключением одного момента в листинга номер 5(считать сверху вниз)

                              public:
                              Container( T* c, M m): m_class( c ), m_method( m ) {}
                              private:
                              T* m_class; M m_method;
                              };


                              Что за магия с объявлением конструктора?
                              Смущает двоеточие после скобки и эти интересные конструкции типа m_class©
                              еще кажется там точки с запятой не хватает после } или это тоже так задумано?(тогда я вообще ничего не понимаю)
                                0
                                С новым годом! Все по плану, так по стандарту C++ вызываются конструкторы полей (fields) класса. Рад, что мой фунаментальный труд до сих пор приносит пользу :). BTW, в C++11 ссоздание делегатов теперь попроще — там есть variadic templates и не нужно делать отдельный шаблон под каждое количество аргументов.
                                  0
                                  С Новым Годом!
                                  Статья актуальна до тех пор, пока есть люди(как я) которые начинают разбираться в шаблонах С++.
                                  В общем то я еще вчера хорошо подумал, погуглил и понял, что мой предыдущий вопрос это список инициализации конструктора и ничего хитрого там нет…
                                  Но я очень рад, что есть возможность пообщаться с автором, потому что следующий вопрос получился более специфичным и нагуглить его не так просто, как предыдущий…

                                  Не могу толком понять механизм извлечения типа из сигнатуры(это последний листинг из тех что в виде картинки).
                                  Сигнатуру я понимаю так(может быть я и тут ошибаюсь): допустим есть метод void method(int param) его сигнатура это void(int)
                                  Следовательно все что нам нужно — это извлечь тип аргумента из сигнатуры. Для этого необходимо воспользоваться трюком с частичной специализацией шаблонов


                                  // Специализация для метода с одним аргументом.
                                  template< class T, class A1 >
                                  class Container< T, void (T::*)(A1) >: public IContainer
                                  {
                                  typedef void (T::*M)(A1);
                                  typedef Arguments A;

                                  особое непонимание вызывает конструкция void (T::*)(A1) которая вроде как описывает указатель на функцию возвращающую тип void… по всей виимости T::* означает что эта функция — член(метод) класса T c параметром A1? Хотя именно запись T::* мне не понятна… Указатель из пространства имен T?
                                  Тут требуется подсказка…

                                  И дальше хитрые typedef… особенно первый. Зачем он? На первый взгляд кажется, что после него void будет эквивалентен (T::*M)(A1)...?
                                  второй как-то более привычно выглядит и кажется делая алиас для типа Arguments, называя его просто A для удобства.

                                  А за предыдущий вопрос прошу прощения, он, конечно же, не стоил вашего времени, потраченного на ответ.
                                    0
                                    Механизм получения типа из сигнатуры подробно раписан в моей сателлитной статье, там как раз ссылка есть перед этим страшным листингом. T-четыре-точки-зведочка это указатель на метод класса T, эволюция там примерно такая:

                                    void (*)(int) // указатель на функцию
                                    void (Myclass::*)(int) // указатель на метод класса Myclass
                                    void (T::*)(A1) // указатель на метод произвольного класса T с типом аргумента A1
                                    


                                    typedef — это всего лишь сокращение кода, чтобы вместо вот этого длинного типа писать короткое «M»:

                                    void (Myclass::* foo)(int) = 0; // без typedef
                                    typedef void (Myclass::* M)(int);
                                    M foo = 0; // с typedef
                                    
                                      0
                                      Большое спасибо!
                                      Благодаря вашим подсказкам теперь наконец полностью разобрался в вашем коде и его работе.

                                      Долго не мог понять, как именно M в записи typedef void (T::*M)(A1); будет именем(алиасом) типа.
                                      Обычно привык видеть что-то вроде typedef int my_int; т.е. ожидал пробел между типом и алиасом.
                                    +1
                                    То, что предлагает Григорий — лучшее из всего, что есть на «рынке»!
                                    0
                                    Это аналог инициализации, видите ли если в классе могут быть атрибуты ввиде ссылок или с пометкой const. Синтаксис С++ запрещает создание неинициализированой ссылки или модификацию константы, то есть нужна инициализации, а в теле конструктора можно только присвоить, тут то и приходит на помощь такой синтаксис.

                                  Only users with full accounts can post comments. Log in, please.