4 марта 2013 в 02:01

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

C++*
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
Vladislav Khorev @Mephi1984
карма
21,0
рейтинг 0,0
Developer
Похожие публикации

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

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