#20 dyvniy » Пн, 27 октября 2014, 13:35:55
Boost::signals сравнимы с сигналами Qt, однако не требуют MOC.
Вообще использование сигналов крайне полезно для проектирования качественного кода с низкой связанностью классов.
http://habrahabr.ru/post/171471/- Спойлер
- О чем эта статья
Сегодня я расскажу про библиотеку 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
с++, c++11, boost, signals, slots
+51 22170
223Mephi1984 10,0
Похожие публикации
Использовать Lua c С++ легче, чем вы думаете. Tutorial по LuaBridge 20 сентября в 19:11
Анимации на лямбдах в C++11 30 июля в 01:13
Спецификатор constexpr в C++11 и в C++14 3 июля в 23:56
Зачем и как я писал BOSS'а и что из этого получилось. Кроссплатформенная система плагинов на C++11 14 мая в 00:56
Сайт на с++ (CppCMS). Часть 1 7 мая в 11:56
Минимализм удаленного взаимодействия на C++11 14 апреля в 15:47
Интерпретация во время компиляции, или Альтернативное понимание лямбд в C++11 9 апреля в 00:11
RMI средствами С++ и boost.preprocessor 29 октября 2013 в 13:48
Реализация Qt signal/slot на Android 6 ноября 2012 в 00:11
DSL для boost::MPL, превращаем f(x) в f<x>::type 7 марта 2010 в 09:41
Комментарии (50)
+1 Door, 4 марта 2013 в 02:24 (комментарий был изменён)#
Я правильно понимаю, что вместо int вот тут boost::signal<int(), Sum> signal должен быть string?
Спасибо за статью, сигналы, возвращающие объекты порадовали… интересно, кто-то использовал эту возможность не на примерах?
0 Mephi1984, 4 марта 2013 в 08:27#↵↑
Спасибо, исправил.
Я использую сигналы и слоты в UI в своем движке. Очень удобно. Уже словил, кстати, несколько грабель — например, нельзя во время вызова сигнала что-то подключать к нему и отключать.
+1 Monnoroch, 4 марта 2013 в 02:31#
Вот блин, а я свой велосипед писал…
+2 Door, 4 марта 2013 в 02:38 (комментарий был изменён)#↵↑
думаю, все через это проходили, куда же без этого, а потом оказывалось, что всё уже давно есть в std::tr1::function, например.
0 lisyarus, 6 марта 2013 в 15:21#↵↑
В с++11 уже и std::function
+2 oYASo, 4 марта 2013 в 02:57 (комментарий был изменён)#
Также стоит дополнить, что сигналы/слоты из boost отлично дружат с сигналами/слотами Qt. Довольно часто это бывает необходимо.
За время использования сигналов/слотов в Qt, у меня сложилось к ним неоднозначное мнение. С одной стороны, это действительно удобный и интуитивно понятный способ вызова одних объектов из других. Так и напрашиваются повесить их на кнопку, тестовое поле или что-то еще.
С другой стороны, сигналы «стреляют во Вселенную», чем очень часто пользуются разработчики. И вот тут начинается дикий геморрой, когда сигнал из одного объекта ловится совершенно никак не относящимся к нему другим объектом, от него уходит еще куда-то и т.д. И так получается, что все объекты системы взаимодействуют между собой только посредством сигналов и слотов, никакого классического ООП.
Я реально такое видел, и исправлять там что-то обычно бессмысленно — проще и лучше написать все заново.
Я не призываю не использовать сигналы/слоты, я призываю использовать их с умом.
+12 Zigmar, 4 марта 2013 в 04:24#↵↑
Как раз независимые объекты, которые обмениваются сообщениями — это самая что не на есть классика ООП. Вызов методов как в С-подобных языках это всего-лишь упрощенная реализация этого механизма. В Smalltalk, например, кажется вообще нету прямого вызова функций (да и функций как таковых) как в процедуральных языках.
+1 ncix, 4 марта 2013 в 23:43 (комментарий был изменён)#↵↑
Поддержу вас. Сигналы-слоты это хоть и неявное но связывание объектов, причем плохоконтролируемое. Можно связать хобот слона с его задницей, и даже не заметить сразу такого конфуза.
Поэтому да, сильно увлекаться не стоит. Механизм мощный, а потому его неосмотрительное использование разрушительно.
+3 JustLuckyGuy, 4 марта 2013 в 05:51#
А как быть с потоками? Ведь слоты вызываются в треде сигнала. Можно ли переложить это в «поток обьекта»?
+2 Mephi1984, 4 марта 2013 в 09:00#↵↑
Я для этого использую 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 rg_software, 4 марта 2013 в 09:17#↵↑
Я подробностей не изучал, но есть ещё библиотека Signals2, которая «thread-safe version of Signals». Вероятно, там эти вопросы прорабатываются.
0 Ryadovoy, 4 марта 2013 в 16:59#↵↑
Signals 2 — потокобезопасная реализация с тем-же интерфейсом что и у Signals, вопросами диспетчеризации сообщений между потоками она, к сожалению, не занимается.
+2 AxisPod, 4 марта 2013 в 10:06#
Хм, а почему не boost::signals2?
+1 Qbit, 4 марта 2013 в 11:29#↵↑
И почему boost::bind вместо std::bind? И почему версия 1.51.0 вместо актуальной?
0 Mephi1984, 4 марта 2013 в 11:33#↵↑
Версию исправил.
std::bind не очень хорошо работает в Visual Studio 2010, поэтому я использую boost::bind
0 Door, 4 марта 2013 в 11:35#↵↑
а в каком плане «не очень хорошо работает в Visual Studio 2010»?
0 Mephi1984, 4 марта 2013 в 11:51#↵↑
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 AxisPod, 4 марта 2013 в 13:14#↵↑
Потому что _1 находится в пространстве std::placeholders, вроде с s на конце. А в бусте в boost.
0 Mephi1984, 4 марта 2013 в 13:29#↵↑
Спасибо, исправил _1 на std::placeholders::_1 — теперь заработало. Буду знать.
НЛО прилетело и опубликовало эту надпись здесь
0 BlackRaven86, 4 марта 2013 в 12:33#↵↑
А разве оно не только под windows?
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
+2 Mephi1984, 4 марта 2013 в 13:50#↵↑
Я посмотрел С++ версию 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 dima_mendeleev, 4 марта 2013 в 13:58#↵↑
Было бы круто, ввести на хабре обязательным пояснение почему + или -. И складывать эти пояснения где-то возле ответа (но это уже детали дизайна). Тогда бы думали перед тем как тыкать.
0 Robotex, 4 марта 2013 в 12:27#
А какова цена этого комфорта? Что у них с производительностью?
+1 Mephi1984, 4 марта 2013 в 12:54#↵↑
Я вот нашел сравнение: timj.testbit.eu/2013/01/25/cpp11-signal-system-performance/
Вывод — Boost Signals это не самая быстрая реализация сигналов и слотов.
+2 tangro, 4 марта 2013 в 13:20#↵↑
Там разница — в наносекунды. Если значения таких порядков важны — ну тогда уж надо хранить указатели на функции в массиве и вручную вызывать, а еще лучше — сразу джампами на асме писать. А в общем случае — какая разница вызовется обработчик OnKeypressed через 60 наносекунд, или через 200, если человек физически имеет реакцию на уровне 20-50 милисекунд в лучшем случае (это на 6 порядков медленнее).
0 Robotex, 4 марта 2013 в 13:20#↵↑
А по сравнению с Qt Slot/Signals?
0 yshurik, 5 марта 2013 в 03:06 (комментарий был изменён)#↵↑
Когда-то давно (когда были первые версии Qt4) пробегало тестирование сигнал-слотов — скорость около пару-десяти милионов в секунду. Скорость конечно заметно ниже просто прямых вызовов так как Qt4 сигналы завязаны на стринговые сигнатуры. В Qt5 они уже пользуют подход близкий или аналогичный boost.
0 EvilsInterrupt, 4 марта 2013 в 12:47#
Автору: А можете также лаконично и кратко описать новую Boost.Coroutine?
+2 Mephi1984, 4 марта 2013 в 12:52#↵↑
Когда изучу — опишу обязательно!
+1 dima_mendeleev, 4 марта 2013 в 14:00#
>… Про Boost Bind я, вероятно, напишу отдельную статью…
А вы замените его на анонимную функцию.
+2 Athari, 4 марта 2013 в 14:07#
boost::signal<void(int, int)> SelectCell;
Забавно, что в шарпе события и обработчики «родные» для языка, а вот такого красивого и лаконичного синтаксиса там нет. События вообще не first-class citizen, а какой-то костыль. Тот же disconnect_all_slots чёрта с два нормально сделаешь, с несоответствием типов постоянные проблемы. Про проверки на null даже вспоминать не хочется. И ничего в этом направлении не происходит, даже супер-продвинутый Rx Framework с блэкджеком и шлюхами работает с событиями через отражения — ужас на курьих ножках. 
Кстати, спортивный интерес. Вот допустим, у меня контрол, в котором 150 событий — можно ли как-то свалить все слоты в один объект и сэкономить на 150 объектах сигналов?
+2 Mephi1984, 4 марта 2013 в 14:20 (комментарий был изменён)#↵↑
>Вот допустим, у меня контрол, в котором 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 Mephi1984, 4 марта 2013 в 14:32 (комментарий был изменён)#↵↑
Пример использования:
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 a553, 4 марта 2013 в 14:21#↵↑
Тот же disconnect_all_slots чёрта с два нормально сделаешь
На мой взгляд, это именно disconnect_all_slots – костыль, потому что он позволяет снять все обработчики/слоты внешнему классу (нарушение инкапсуляции), и простого способа предотвратить это, как я понимаю, нет.
В .NET же это просто и удобно:
class MyClass
{
event EventHandler MyEvent;
void MyMethod()
{
this.MyEvent = null;
}
}
НЛО прилетело и опубликовало эту надпись здесь
0 Athari, 4 марта 2013 в 17:28#↵↑
Обойтись можно, и в основном этим способом и пользуюсь, но синтаксис ужасный. Ну почему нельзя в язык добавить нормальный доступ к add/remove по имени события? Проблем с обратной совместимостью, вроде, быть не должно; и в целом цена фичи выглядит небольшой.
НЛО прилетело и опубликовало эту надпись здесь
0 Athari, 4 марта 2013 в 18:34#↵↑
Имея имя события, нельзя обратиться к add и remove как методам этого события. Нельзя передать событие как аргумент: «Вот тебе событие, подпишись на него».
0 a553, 4 марта 2013 в 19:22 (комментарий был изменён)#↵↑
Вот так вы можете передать событие в функцию, а также подписаться на него:
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 Athari, 4 марта 2013 в 19:40#↵↑
Вот так вы можете передать событие в функцию, а также подписаться на него:
Возможно только внутри класса, который определяет событие.
Вот так вы, имея имя события, можете получить add_MyEvent
Дык отражения же, по сути хак — ни строгой типизации, ни нормального рефакторинга. О том и речь.
0 a553, 4 марта 2013 в 20:30 (комментарий был изменён)#↵↑
Возможно только внутри класса, который определяет событие.
Ссылку за пределы класса можно вывести через callback-и. Несколько неудобно, да. С другой стороны, мне ещё никогда не приходилось передавать событие как аргумент. Предпочитаю IoC событийно-ориентированному подходу.
0 Athari, 4 марта 2013 в 21:05#↵↑
С другой стороны, мне ещё никогда не приходилось передавать событие как аргумент.
Reactive Extensions не доводилось пользоваться? В основном на стыке между Rx и традиционным кодом с событиями такая проблема и возникает. IoC и коллбэки проблему не решают, потому что в .NET события везде и всюду, свои решения в сам фреймворк не запихнуть.
0 a553, 4 марта 2013 в 21:36#↵↑
Нет, не доводилось. Когда-то хотел познакомиться, но, посмотрев в код реального проекта и увидев монструозные малопонятные конструкции, я быстро ретировался. С тех пор и использую везде IoC – и в ASP.NET, и в WPF – и прекрасно себя чувствую.
0 braindamaged, 4 марта 2013 в 22:38#↵↑
> Кстати, спортивный интерес. Вот допустим, у меня контрол, в котором 150 событий
Если вы про .net, то посмотрите на WinForns, там как раз так и организовано, чтобы не возить с собой 100500 объектов событий, на большинство которых так никто и не подпишется (т.н. «sparse events»)
0 6opoDuJIo, 5 марта 2013 в 14:28#
Я правильно понимаю что сингалы — это те-же многоадресные делегаты?
