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, например.
    • +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
                                        Я правильно понимаю что сингалы — это те-же многоадресные делегаты?

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