Boost Signals — сигналы и слоты для C++

  • Tutorial
image

О чем эта статья


Сегодня я расскажу про библиотеку Boost Signals — про сигналы, слоты, соединения, и как их использовать.

Сигнал — это тип данных, который может хранить в себе несколько функций обратного вызова, и вызывать их.
Слот — это, соответственно, и есть подсоединяемые к сигналу функции.
Как уже было сказано, к одному сигналу можно подключить несколько функции, и при вызове сигнала, подключенные функции вызываются в порядке их подключения.
Похожую систему иногда называют событийной, тогда сигналы — это события, а слоты — это подписанные на определенные события обработчики.



Простой пример


Допустим, мы делаем UI для игры. У нас в игре будет много кнопок, и каждая кнопка по нажатию будет выполнять определенные действия. И хотелось бы при этом, чтобы все кнопки принадлежали одному типу Button — то есть требуется отделить кнопку от выполняемого по нажатию на нее кода. Как раз для такого разделения и нужны сигналы.

Объявляется сигнал очень просто. Объявим его как член класса:
#include "boost/signals.hpp"

class Button
{
public:
    boost::signal<void()> OnPressed; //Сигнал
};


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

void FunctionSlot()
{
    std::cout<<"FunctionSlot called"<<std::endl;
}

...

Button mainButton;

mainButton.OnPressed.connect(&FunctionSlot); //Подключаем слот


Кроме функции, можно подключить также функциональный объект:
struct FunctionObjectSlot
{
    void operator()()
    {
        std::cout<<"FunctionObjectSlot called"<<std::endl;
    }
};

...

//Подключаем функциональный объект
mainButton.OnPressed.connect(FunctionObjectSlot());

Иногда, если кода очень мало, удобнее писать анонимную функцию и сразу же ее подключать:
//Подключаем анонимную функцию
mainButton.OnPressed.connect([]() { std::cout<<"Anonymous function is called"<<std::endl; });


Если необходимо вызвать метод объекта — его тоже можно подключить, воспользовавшись синтаксисом boost::bind:
#include "boost/bind.hpp"

class MethodSlotClass
{
public:
    void MethodSlot()
    {
        std::cout<<"MethodSlot is called"<<std::endl;
    }
};

...

MethodSlotClass methodSlotObject;

//Подключаем метод
mainButton.OnPressed.connect(boost::bind(&MethodSlotClass::MethodSlot, &methodSlotObject));


Про Boost Bind я, вероятно, напишу отдельную статью.
Таким образом, мы подключили к сигналу сразу несколько слотов. Для того, чтобы «послать» сигнал, следует вызвать оператор скобки () для сигнала:
mainButton.OnPressed();


При этом слоты будут вызваны в порядке их подключения. В нашем случае вывод будет таким:
FunctionSlot called
FunctionObjectSlot called
Anonymous function is called
MethodSlot is called


Сигналы с параметрами


Сигналы могут содержать параметры. Вот пример объявления слота, который содержит параметры:
boost::signal<void(int, int)> SelectCell;


В этом случае, очевидно, и функции должны быть с параметрами:
void OnPlayerSelectCell(int x, int y)
{
    std::cout<<"Player selected cell: "<<x<<", "<<y<<std::endl;
}

//Передаем функцию с параметрами:
SelectCell.connect(&OnPlayerSelectCell);

//Или так:
SelectCell.connect([](int x, int y) { std::cout<<"Player selected cell: "<<x<<", "<<y<<std::endl; });

//Вызываем сигнал с параметрами:
SelectCell(10, -10);


Сигналы, возвращающие объекты


Сигналы могут возвращать объекты. С этим связана одна тонкость — если вызвано несколько слотов, то ведь, в сущности, возвращается несколько объектов, не так ли? Но сигнал, в свою очередь, может вернуть только один объект. По умолчанию сигнал возвращает объект, который был получен от последнего слота. Однако, мы можем передать в сигнал свой собственный «агрегатор», который скомпонует возвращенные объекты в одно.

Я, конечно, не встречал ситуации, когда мне требуется возвращать значения от сигнала, ну да ладно. Предположим, в нашем случае сигнал вызывает слоты, которые возвращают строки, а сигнал должен вернуть строку, склеенную из этих строк.
Пишется такой агрегатор просто — нужно создать структуру с шаблонных оператором «скобки», принимающим в качестве параметров итераторы:

struct Sum
{
    template<typename InputIterator>
    std::string operator()(InputIterator first, InputIterator last) const
    {
        //Нет слотов - возвращаем пустую строку:
        if (first == last)
        {
            return std::string();
        }

        //Иначе - возвращаем сумму строк:
        std::string sum;
        while (first != last)
        {
            sum += *first;
            ++first;
        }

        return sum;
    }
};


//Функции для проверки:

auto f1 = []() -> std::string
{
    return "Hello ";
};

auto f2 = []() -> std::string
{
    return "World!";
};

boost::signal<std::string(), Sum> signal;

signal.connect(f1);
signal.connect(f2);

std::cout<<signal()<<std::endl; //Выводит "Hello World!"


Отключение сигналов


Для того, чтобы отключить все сигналы от слота, следует вызвать метод disconnect_all_slots.
Для того, чтобы управлять отдельным слотом, придется при подключении слота создавать отдельный объект типа boost::connection.
Примеры:

//Отключаем все слоты
mainButton.OnPressed.disconnect_all_slots();

//Создаем соеднинение с слотом FunctionSlot
boost::signals::connection con = mainButton.OnPressed.connect(&FunctionSlot);

//Проверяем соединение
if (con.connected())
{
    //FunctionSlot все еще подключен.
    mainButton.OnPressed(); //Выводит "FunctionSlot called"
}

con.disconnect(); // Отключаем слот

mainButton.OnPressed(); //Не выводит ничего


Порядок вызова сигналов


Не знаю, когда это может пригодиться, но при подключении слотов можно указать порядок вызова:

mainButton.OnPressed.connect(1, &FunctionSlot);
mainButton.OnPressed.connect(0, FunctionObjectSlot());

mainButton.OnPressed(); //Вызовет сначала "FunctionObjectSlot called", а затем "FunctionSlot called"


Заключение


Сигналы и слоты очень удобны в том случае, когда нужно уменьшить связность различных объектов. Раньше, чтобы вызывать одни объекты из других, я передавал указатели на одни объекты в другие объекты, и это вызывало циклические ссылки и превращало мой код в кашу. Теперь я использую сигналы, которые позволяют протянуть тонкие «мостики» между независимыми объектами, и это здорово уменьшило связность моего кода. Используйте сигналы и слоты на здоровье!

Список использованной литературы:


www.boost.org/doc/libs/1_53_0/doc/html/signals.html
Поделиться публикацией
Комментарии 50
    +1
    Я правильно понимаю, что вместо int вот тут boost::signal<int(), Sum> signal должен быть string?
    Спасибо за статью, сигналы, возвращающие объекты порадовали… интересно, кто-то использовал эту возможность не на примерах?
      0
      Спасибо, исправил.
      Я использую сигналы и слоты в UI в своем движке. Очень удобно. Уже словил, кстати, несколько грабель — например, нельзя во время вызова сигнала что-то подключать к нему и отключать.
      +1
      Вот блин, а я свой велосипед писал…
        +2
        думаю, все через это проходили, куда же без этого, а потом оказывалось, что всё уже давно есть в std::tr1::function, например.
          0
          В с++11 уже и std::function
        +2
        Также стоит дополнить, что сигналы/слоты из boost отлично дружат с сигналами/слотами Qt. Довольно часто это бывает необходимо.

        За время использования сигналов/слотов в Qt, у меня сложилось к ним неоднозначное мнение. С одной стороны, это действительно удобный и интуитивно понятный способ вызова одних объектов из других. Так и напрашиваются повесить их на кнопку, тестовое поле или что-то еще.
        С другой стороны, сигналы «стреляют во Вселенную», чем очень часто пользуются разработчики. И вот тут начинается дикий геморрой, когда сигнал из одного объекта ловится совершенно никак не относящимся к нему другим объектом, от него уходит еще куда-то и т.д. И так получается, что все объекты системы взаимодействуют между собой только посредством сигналов и слотов, никакого классического ООП.
        Я реально такое видел, и исправлять там что-то обычно бессмысленно — проще и лучше написать все заново.
        Я не призываю не использовать сигналы/слоты, я призываю использовать их с умом.
          +12
          Как раз независимые объекты, которые обмениваются сообщениями — это самая что не на есть классика ООП. Вызов методов как в С-подобных языках это всего-лишь упрощенная реализация этого механизма. В Smalltalk, например, кажется вообще нету прямого вызова функций (да и функций как таковых) как в процедуральных языках.
            +1
            Поддержу вас. Сигналы-слоты это хоть и неявное но связывание объектов, причем плохоконтролируемое. Можно связать хобот слона с его задницей, и даже не заметить сразу такого конфуза.
            Поэтому да, сильно увлекаться не стоит. Механизм мощный, а потому его неосмотрительное использование разрушительно.
            +3
            А как быть с потоками? Ведь слоты вызываются в треде сигнала. Можно ли переложить это в «поток обьекта»?
              +2
              Я для этого использую boost::asio.
              В основном потоке запускаю IoService, который вызывает run_one, а все вызовы сигнала заворачиваю в IoService.post. Получается как-то так, например:

              boost::asio::io_service IoService;
              
              boost::signal<void(int int)> TapDownSignal;
              
              //В чужом потоке
              void Application::OnTapDown(int x, int y)
              {
                  IoService.post(boost::bind(boost::ref(TapDownSignal), x, y));
              }
              
              
              //В основном потоке:
              void ResourceManager::Update(int dt)
              {
                  ...
                  IoService.run_one();
              }
              
                0
                Я подробностей не изучал, но есть ещё библиотека Signals2, которая «thread-safe version of Signals». Вероятно, там эти вопросы прорабатываются.
                  0
                  Signals 2 — потокобезопасная реализация с тем-же интерфейсом что и у Signals, вопросами диспетчеризации сообщений между потоками она, к сожалению, не занимается.
                +2
                Хм, а почему не boost::signals2?
                  +1
                  И почему boost::bind вместо std::bind? И почему версия 1.51.0 вместо актуальной?
                    0
                    Версию исправил.
                    std::bind не очень хорошо работает в Visual Studio 2010, поэтому я использую boost::bind
                      0
                      а в каком плане «не очень хорошо работает в Visual Studio 2010»?
                        0
                        struct MyStruct
                        {
                        void method(int x)
                        {
                        }
                        };
                        
                        boost::signal<void(int x)> mySignal;
                        
                        MyStruct myStruct;
                        
                        mySignal.connect(std::bind(&MyStruct::method, &myStruct, _1));
                        


                        Компилятор ругается на последнюю строку многоэтажной ошибкой. Я ниасилил понять эту ошибку, поэтому избегаю std::bind.
                          +5
                          Потому что _1 находится в пространстве std::placeholders, вроде с s на конце. А в бусте в boost.
                            0
                            Спасибо, исправил _1 на std::placeholders::_1 — теперь заработало. Буду знать.
                • НЛО прилетело и опубликовало эту надпись здесь
                    0
                    А разве оно не только под windows?
                    • НЛО прилетело и опубликовало эту надпись здесь
                    • НЛО прилетело и опубликовало эту надпись здесь
                        +2
                        Я посмотрел С++ версию Rx Framework (https://rx.codeplex.com/SourceControl/changeset/view/7881e17c060b#Rx/CPP/RxCpp.sln) — это оно? Я не нашел способа скомпилировать это под Android и iOS, а это для меня критично.

                        >Нельзя получить сигнал, который бы аггрегировал другие сигналы без кучи boilerplate кода.
                        boost::signal<void()> signal1;
                        boost::signal<void()> signal2;
                        
                        signal2.connect(boost::ref(signal1));
                        
                        signal2(); //Вызывает signal2, который вызывает signal1
                        

                        Это оно?

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

                        • НЛО прилетело и опубликовало эту надпись здесь
                          +3
                          Было бы круто, ввести на хабре обязательным пояснение почему + или -. И складывать эти пояснения где-то возле ответа (но это уже детали дизайна). Тогда бы думали перед тем как тыкать.
                      • НЛО прилетело и опубликовало эту надпись здесь
                          +1
                          Я вот нашел сравнение: timj.testbit.eu/2013/01/25/cpp11-signal-system-performance/
                          Вывод — Boost Signals это не самая быстрая реализация сигналов и слотов.
                            +2
                            Там разница — в наносекунды. Если значения таких порядков важны — ну тогда уж надо хранить указатели на функции в массиве и вручную вызывать, а еще лучше — сразу джампами на асме писать. А в общем случае — какая разница вызовется обработчик OnKeypressed через 60 наносекунд, или через 200, если человек физически имеет реакцию на уровне 20-50 милисекунд в лучшем случае (это на 6 порядков медленнее).
                            • НЛО прилетело и опубликовало эту надпись здесь
                                0
                                Когда-то давно (когда были первые версии Qt4) пробегало тестирование сигнал-слотов — скорость около пару-десяти милионов в секунду. Скорость конечно заметно ниже просто прямых вызовов так как Qt4 сигналы завязаны на стринговые сигнатуры. В Qt5 они уже пользуют подход близкий или аналогичный boost.
                            0
                            Автору: А можете также лаконично и кратко описать новую Boost.Coroutine?
                              +2
                              Когда изучу — опишу обязательно!
                              +1
                              >… Про Boost Bind я, вероятно, напишу отдельную статью…
                              А вы замените его на анонимную функцию.
                                +2
                                boost::signal<void(int, int)> SelectCell;

                                Забавно, что в шарпе события и обработчики «родные» для языка, а вот такого красивого и лаконичного синтаксиса там нет. События вообще не first-class citizen, а какой-то костыль. Тот же disconnect_all_slots чёрта с два нормально сделаешь, с несоответствием типов постоянные проблемы. Про проверки на null даже вспоминать не хочется. И ничего в этом направлении не происходит, даже супер-продвинутый Rx Framework с блэкджеком и шлюхами работает с событиями через отражения — ужас на курьих ножках. :(

                                Кстати, спортивный интерес. Вот допустим, у меня контрол, в котором 150 событий — можно ли как-то свалить все слоты в один объект и сэкономить на 150 объектах сигналов?
                                  +2
                                  >Вот допустим, у меня контрол, в котором 150 событий — можно ли как-то свалить все слоты в один объект и сэкономить на 150 объектах сигналов?

                                  Я делаю комбинацией shared_ptr и variant, не судите строго:

                                  //Variant с зараннее определенными типами данных:
                                  typedef boost::variant<int, float, std::string, vec2> TSignalParam;
                                  
                                  
                                  //Хранитель различных сигналов:
                                  struct TWidgetStruct
                                  {
                                  protected:
                                      //Карта сигналов, ключ - имя сигнала
                                      std::map<std::string, std::shared_ptr<boost::signal<void (TSignalParam)>>> SignalMap; 
                                  
                                  public:
                                  
                                      //Чистим все
                                      void ClearSignals()
                                      {
                                          SignalMap.clear();
                                      }
                                  
                                  	//Добавляем слот к сигналу
                                      void AddSlot(std::string signalName, std::function<void (TSignalParam)>> func)
                                      {
                                          
                                          //Если такого сигнала еще нет - создаем
                                          if (SignalMap[signalName] == std::shared_ptr<boost::signal<void (TSignalParam)>>())
                                          {
                                          	SignalMap[signalName] = std::shared_ptr<boost::signal<void (TSignalParam)>>(
                                                      new boost::signal<void (TSignalParam)>());
                                          }
                                          
                                  	//Добавляем слот
                                          SignalMap[signalName]->connect(func);
                                      }
                                  };
                                  
                                    0
                                    Пример использования:

                                    auto mouseDownFunc = [](TSignalParam param)
                                    {
                                    	vec2 v = boost::get<vec2>(param);
                                    	
                                    	std::cout<<"pressed at "<<v.x<<" "<<v.y<<std::endl;
                                    }
                                    
                                    auto changeTextFunc = [](TSignalParam param)
                                    {
                                    	std::string text = boost::get<std::string>(param);
                                    
                                    	std::cout<<"text :"<<text<<std::endl;
                                    }
                                    
                                    TWidgetStruct WidgetStruct;
                                    
                                    WidgetStruct.AddSlot("OnMouseDown", mouseDownFunc);
                                    WidgetStruct.AddSlot("OnChangeText", changeTextFunc);
                                    
                                    • НЛО прилетело и опубликовало эту надпись здесь
                                      +3
                                      Тот же disconnect_all_slots чёрта с два нормально сделаешь

                                      На мой взгляд, это именно disconnect_all_slots – костыль, потому что он позволяет снять все обработчики/слоты внешнему классу (нарушение инкапсуляции), и простого способа предотвратить это, как я понимаю, нет.

                                      В .NET же это просто и удобно:

                                      class MyClass
                                      {
                                          event EventHandler MyEvent;
                                      
                                          void MyMethod()
                                          {
                                              this.MyEvent = null;
                                          }
                                      }
                                      
                                      • НЛО прилетело и опубликовало эту надпись здесь
                                          0
                                          Обойтись можно, и в основном этим способом и пользуюсь, но синтаксис ужасный. Ну почему нельзя в язык добавить нормальный доступ к add/remove по имени события? Проблем с обратной совместимостью, вроде, быть не должно; и в целом цена фичи выглядит небольшой.
                                          • НЛО прилетело и опубликовало эту надпись здесь
                                              0
                                              Имея имя события, нельзя обратиться к add и remove как методам этого события. Нельзя передать событие как аргумент: «Вот тебе событие, подпишись на него».
                                                0
                                                Вот так вы можете передать событие в функцию, а также подписаться на него:

                                                class MyClass
                                                {
                                                    event EventHandler MyEvent;
                                                
                                                    void MyMethod()
                                                    {
                                                        this.Subscribe(ref this.MyEvent, this.MyHandler);
                                                    }
                                                
                                                    // код аналогичен add_MyEvent
                                                    // можно переписать в общем виде, используя касты в System.Delegate
                                                    void Subscribe(ref EventHandler e, EventHandler handler)
                                                    {
                                                        EventHandler fetched;
                                                        EventHandler current = e;
                                                        do
                                                        {
                                                            fetched = current;
                                                            EventHandler newE = (EventHandler)Delegate.Combine(fetched, handler);
                                                            current = Interlocked.CompareExchange(ref e, newE, fetched);
                                                        }
                                                        while (current != fetched);
                                                    }
                                                
                                                    void MyHandler(object o, EventArgs e)
                                                    {
                                                    }
                                                }
                                                


                                                Вот так вы, имея имя события, можете получить add_MyEvent:

                                                Action<EventHandler> add_MyEvent =
                                                    (Action<EventHandler>)
                                                    typeof(MyClass)
                                                        .GetEvent("MyEvent", BindingFlags.NonPublic | BindingFlags.Instance)
                                                        .GetAddMethod(true)
                                                        .CreateDelegate(typeof(Action<EventHandler>), myClass);
                                                // myClass – экземпляр MyClass
                                                
                                                  0
                                                  Вот так вы можете передать событие в функцию, а также подписаться на него:

                                                  Возможно только внутри класса, который определяет событие.

                                                  Вот так вы, имея имя события, можете получить add_MyEvent

                                                  Дык отражения же, по сути хак — ни строгой типизации, ни нормального рефакторинга. О том и речь.
                                                    0
                                                    Возможно только внутри класса, который определяет событие.
                                                    Ссылку за пределы класса можно вывести через callback-и. Несколько неудобно, да. С другой стороны, мне ещё никогда не приходилось передавать событие как аргумент. Предпочитаю IoC событийно-ориентированному подходу.
                                                      0
                                                      С другой стороны, мне ещё никогда не приходилось передавать событие как аргумент.

                                                      Reactive Extensions не доводилось пользоваться? В основном на стыке между Rx и традиционным кодом с событиями такая проблема и возникает. IoC и коллбэки проблему не решают, потому что в .NET события везде и всюду, свои решения в сам фреймворк не запихнуть.
                                                        0
                                                        Нет, не доводилось. Когда-то хотел познакомиться, но, посмотрев в код реального проекта и увидев монструозные малопонятные конструкции, я быстро ретировался. С тех пор и использую везде IoC – и в ASP.NET, и в WPF – и прекрасно себя чувствую.
                                          0
                                          > Кстати, спортивный интерес. Вот допустим, у меня контрол, в котором 150 событий

                                          Если вы про .net, то посмотрите на WinForns, там как раз так и организовано, чтобы не возить с собой 100500 объектов событий, на большинство которых так никто и не подпишется (т.н. «sparse events»)
                                          0
                                          Я правильно понимаю что сингалы — это те-же многоадресные делегаты?

                                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                          Самое читаемое