Содержание

Зачем это нужно

Нередко при проектировании систем возникают задачи следующего характера. В системе появляется сущность, являющаяся генератором (источником) некоторых событий. И есть ряд других сущностей, которые должны быть уведомлены о возникновении этих событий. Простейший пример – на некоторой интерфейсной форме есть кнопка, по клику на которой необходимо выполнить некоторый код. Кнопка – источник события. Класс формы – получатель уведомления о возникновении события и его обработчик. В книге «Паттерны проектирования» от GoF такое взаимодействие сущностей описано как паттерн «Команда» или «Действие». Непосредственной поддержки описанного механизма подписки-уведомления в языке С++ (в отличии от C#, например) нет. Но реализацию этого механизма можно найти в boost. А именно – в библиотеке boost::signal. Описанный простейший пример с кнопкой и формой будет выглядеть следующим образом:

#include <iostream>
#include <boost/signal.hpp>
#include <boost/bind.hpp>
 
//////////////////////////////////////////////////////////////////////////////////////////
// Класс кнопки
// Пример класса-источника событий
//////////////////////////////////////////////////////////////////////////////////////////
class SampleButton
{
public:
	// Метод, добавляющий новый обработчик события
	// h - функтор, реализующий обработчик
	void SetOnClick(boost::function<void()> h) 
	{
		// Присоединяем к событию новый обоаботчик
		m_OnClick.connect(h);
	}
	// Метод, инициирущий событие
	void FireOnClick() {m_OnClick();}
private:
	// Член данных, хранящий коллекцию обработчиков события OnClick
	boost::signal<void ()> m_OnClick;
};
 
//////////////////////////////////////////////////////////////////////////////////////////
// Класс формы
// Пример класса-обработчика событий
//////////////////////////////////////////////////////////////////////////////////////////
class SampleForm
{
public:
	// Конструктор
	SampleForm()
	{
		// Добавляем к каждой кнопке обработчики события OnClick
		m_Button1.SetOnClick(boost::bind(&SampleForm::Handler1, this));
		m_Button2.SetOnClick(boost::bind(&SampleForm::Handler2, this));
		m_Button3.SetOnClick(boost::bind(&SampleForm::Handler2, this));
		m_Button3.SetOnClick(boost::bind(&SampleForm::Handler1, this));
	}
 
	// Тестовый обработчик события
	void Handler1()
	{
		std::cout << "Handler 1 entered" << std::endl;
	}
 
	// Тестовый обработчик события
	void Handler2()
	{
		std::cout << "Handler 2 entered" << std::endl;
	}
 
	// Пример кнопок
	SampleButton m_Button1;
	SampleButton m_Button2;
	SampleButton m_Button3;
};
 
int main(int argc, char** argv)
{
	// Создаем форму
	SampleForm form;
 
	// Для каждой кнопки на форме инициируем событие OnClick
	form.m_Button1.FireOnClick();
	form.m_Button2.FireOnClick();
	form.m_Button3.FireOnClick();
 
	// Вывод на консоль следующий
	// Handler 1 entered
	// Handler 2 entered
	// Handler 2 entered
	// Handler 1 entered
	return 0;
}

Что, собственно, здесь происходит.

  1. Объявляется член данных, описывающий событие. В этом примере это SampleButton::m_OnClick. Тип этого члена данных – boost::signal. Этот класс – шаблонный, и в качестве обязательного параметра принимает сигнатуру обработчика сигнала. В примере эта сигнатура – void (). Т. е. обработчик не принимает параметров и ничего не возвращает.
  2. Реализуется метод, осуществляющий подписку на соответствующее событие. В примере – это метод AddOnClick. Подписка осуществляется с помощью метода signal::connect, в который передается обработчик, с которым необходимо связать событие.
  3. Где-то в коде производится инициация события. В данном случае это делается в методе SampleButton::FireOnClick. Инициация события производится путем применения оператора () к члену данных, описывающего событие. В данном случае – к члену m_OnClick.

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

Как видно из примера, на одно событие можно назначить сразу несколько обработчиков. При этом (в простейшем случае) будут вызваны все назначенные обработчики. Правда, последовательность их вызова разработчиками библиотеки не гарантируется. Нередко для обозначения сущности, описывающей событие (члена данных m_OnClick) используется термин сигнал (signal). А для обозначения подписчиков на это событие – термин слот (slot). В частности, эта терминология используется в оригинальной документации на эту библиотеку.

Как это работает

Объявление объекта-сигнала

Объект-сигнал объявляется как экземпляр класса boost::signal<Sig, Comb, Group, GroupComp, SlotFn>. У этого шаблонного класса первый аргумент является обязательным, и определяет сигнатуру вызова сигнала. Оставшиеся – определяют некоторые аспекты поведения объекта-сигнала, о которых будет рассказано ниже. Примеры объявления объекта-сигнала:

boost::signal<void ()> sig; // Сигнал без аргументов, который ничего не возвращает
boost::signal<int ()>sig; // Сигнал без аргументов, возвращающий целое число
boost::signal(void (float, int)> sig; // Сигнал, принимающий два аргумента и ничего не возвращающий

и т. п. Сигнатура сигнала определяет сигнатуру слотов, которые могут быть присоединены к такому сигналу. Вообще говоря, в ряде случаев требуется точное соответствие сигнатур слотов сигнатуре сигнала.

В случае, если в сигнатуре сигнала указывается, что сигнал принимает параметры, то аргументы, переданные при инициации (вызове) сигнала передаются на вход каждому из слотов, присоединенных к сигналу. В случае, если указывается, что сигнал возвращает некое значение, то (в общем случае) возвращается значение, полученное в результате вызова последнего слота в цепочке. Но такое поведение по-умолчанию может быть изменено. Подробнее об этом – в разделе «комбинирование возвращаемых слотами возвращаемых значений».

Присоединение слотов

Слот может быть присоединен к сигналу с помощью метода connect. В общем случае этот метод принимает на вход один аргумент – сущность, реализующую концепцию функционального объекта. Таким образом, к слоту может быть присоединено все что угодно, если к этому примени оператор вызова функции с подходящей сигнатурой. В частности, это означает, что вместе с boost::signal может легко применяться такие компоненты boost, как boost::function и boost::bind.

Указание порядка вызова слотов

В случае простого присоединения нескольких слотов к сигналу библиотека, вообще говоря, не специфицирует, в каком порядке будут вызваны соответствующие слотам функции. Если все слоты присоединяются в одном месте, то порядок предсказать можно (вызываться они будут в порядке присоединения). Если же присоединение слотов «размазано по коду», то такое предсказание сделать гораздо сложнее. И, если порядок вызова слотов важен, то его надо специфицировать явно путем специальной формы вызова метода connect:

float foo(int, int)
{
	return 1.5;
}
 
int bar(float, float)
{
	return 2.5;
}
 
boost::signal<float (int, int)> test_sig;

В случае такого присоедининия слотов:

test_sig.connect(0, &foo);
test_sig.connect(1, &bar);
std::cout << test_sig(1.5, 2.5) << std::endl;

на экран будет выведено число 1.5. А в случае такого присоединения:

test_sig.connect(1, &foo);
test_sig.connect(0, &bar);
std::cout << test_sig(1.5, 2.5) << std::endl;

число 2.

Не сложно понять, что все дело в первом параметре метода connect. В общем случае – это целое число, задающее очередность вызова слотов при инициации сигнала. Но, вообще говоря, это может быть и совсем не целое число, а, например строка. Тип группирующего параметра а также способ их упорядочивания задаются третьим и четвертым шаблонным параметром класса signal (Group и GroupComp). По умолчанию это int и less<int> соответственно. Например:

boost::signal<float (int, int), boost::last_value<float>, std::string> test_sig;
 
test_sig.connect("aaa", &foo);
test_sig.connect("bbb", &bar);
std::cout << test_sig(1.5, 2.5) << std::endl;

Выведет на экран число 2.

У метода connect может быть еще один параметр (последний), определяющий место, куда помещается новый слот при присоединении. Варианты этого параметра – boost::signals::at_front (слот добавляется в начало списка) или boost::signals::at_back (слот добавляется в конец списка). По умолчанию этот параметр равен boost::signals::at_back, т. е. слоты добавляются в конец списка. Если добавляемый слот не содержит идентификатора группы, то (в зависимости от значения этого параметра) он добавляется либо в начало, либо в конец общего списка слотов. В случае указания идентификатора группы этот параметр определяет способ добавления слота в рамках других слотов с тем же идентификатором. Например:

boost::signal<float (int, int), boost::last_value<float>, std::string> test_sig;
 
test_sig.connect("aaa", &foo, boost::signals::at_front);
test_sig.connect("aaa", &bar, boost::signals::at_front);
std::cout << test_sig(1.5, 2.5) << std::endl;

Выведет на экран число 1.5.

Недостатки

Достоинства