О вольностях в ссылках или простейший обмен сообщениями

от автора

Обмен сообщениями достаточно фундаментальная вещь в науке Computer Science. Будем рассматривать её в приближении к событийно-ориентированному программированию (event-driven). Терминология, возможности и реализации могут отличаться: события (events), сообщения (messages), сигналы/слоты (signals/slots) и callbacks. В целом суть, что с приходом события запускается ответная реакция.
Сама система обмена сообщениями в статье послужила демонстрацией вольной, но допустимой интерпретации ссылок/указателей, упрощающей код. Получившаяся система тривиальна и умеет только регистрировать обработчик на определённый код сообщения и посылать сообщения с таким кодом.
Допустим что обработчики нетривиальные, а сообщений немного. И что мы сами генерируем сообщения и они не приходят нам по сети, например. В таком случае хочется иметь что-то более удобное с явными объявлениями переменных в сообщении. Например, нечто подобное:

StringMessage* str_message = ...; send(my_message); ... void handle_message(const Message* message) { 	assert(message); 	const StringMessage* str_message = dynamic_cast<const StringMessage*>(message); 	assert(str_message); 	std::cout << str_message->message ... } 

Но хочется убрать проверочный код, не имеющий отношения к логике работы, под капот. Заменим поэтому указатель на ссылку, показав что в обработчик точно приходит объект, а не NULL nullptr. И пусть обработчик сразу принимает требуемый им тип сообщения.

void handle_message(const StringMessage& message) { 	... } 

Как осуществить задуманное и поддержать другие возможные классы сообщений?

Идея проста. Во время регистрации обработчика узнаем тип аргумента, который он принимает и запишем его. А при отсылке сообщения проверим, что тип сообщения совпадает с типом аргумента обработчика. Для каждого нового типа сообщения пронаследуемся от базового класса сообщения Message.

class Message{ public: 	Message(unsigned code) : code(code) {} 	virtual ~Message() {} 	const unsigned code; };  enum Code { 	STRING = 1 };  class StringMessage : public Message { public: 	StringMessage(const std::string& msg) : Message(STRING), message(msg) {} 	const std::string message; }; 

Решение с делегатами

Старые добрые делегаты работают в С++03. Один из примеров реализации описан на Хабре здесь. Делегаты в данном случае это только функциональная обёртка над функциями-членами. Так выглядит подписка обработчика.

class Messenger { 	... 	template <class T, class MessageDerived> 	void subscribe(int code, T* object, void (T::* method)(const MessageDerived&)) { 		// Сохраняем тип аргумента, который действительно принимает функция-член класса 		const std::type_index& arg_type = typeid(const MessageDerived); 	 		// Преобразуем указатель функцию, как будто он принимает просто (const Message&) 		void (T::* sign)(const Message&) = (void (T::*)(const MessageDerived&)) method; 				 		// Добавляем нового подписчика 		subscribers_.push_back(Subscriber(code, object, NewDelegate(object, sign), arg_type)); 	} } 

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

template <class Base, class Derived> bool is_sliced(const Derived* der) { 	return (void*) der != (const Base*) der; } 

Но лучше всего написать проверку времени компиляции. Компилятор сделает срез базового типа по отнаследованному. И если указатель увеличился с 1, значит объект был срезан.

template <class Base, class Derived> struct is_sliced2 : public std::integral_constant<bool,  	((void*)((Base*)((Derived*) 1))) != (void*)1> {}; ... static_assert(!is_sliced2<Message, Arg>::value, "Message object should not be sliced"); 

К сожалению, компилятор MSVS 2013 не справляется с компиляцией условия, но gcc-4.8.1 вполне.

Отправление сообщения делаем просто. Проверяем, что сообщение не срезается. Пробегаем по всем обработчикам. Если коды сообщения и обработчика совпадают, то проверяем типы на соответствие. Если всё совпало, то вызываем обработчик.

Отправка сообщения

class Messenger { 	... 	template <class LikeMessage> 	void send(const LikeMessage& msg) { 		assert((!is_sliced<Message, LikeMessage>(&msg))); 		send_impl(msg); 	}  private: 	void send_impl(const Message& msg) { 		const std::type_info& arg = typeid(msg); // Кешируем настоящий тип сообщения 		for (SubscribersCI i = subscribers_.begin(); i != subscribers_.end(); ++i) { 			if (i->code == msg.code) {           // Нашли требуемый код 				if (arg != i->arg_type)          // Плохо, если не совпали типы аргумента и делегата 					throw std::logic_error("Bad message cast"); 				i->method->call(msg);            // Вызывается ф-я член  			} 		} 	} } 

Важно не забыть добавить проверку, что MessageDerived действительно унаследован от Message. В С++11 в файле <type_traits> есть std::is_base_of. В С++03 проверку времени компиляции придётся писать руками.
Пример с делегатом простой. Класс обработчика, подписка делегата и отправление сообщения:

class Printer { public: 	void print(const StringMessage& msg) { 		std::cout << "Printer received: " << msg.message << std::endl; 	} };  int main() { 	Messenger messenger; 	Printer print; 	messenger.subscribe(STRING, &print, &Printer::print); 	messenger.send(StringMessage("Hello, messages!")); 	return 0; } 

Код с делегатами

C++11

В C++11 появились лямбды. Наша цель, чтобы процесс подписки выглядел очень просто:

messenger.subscribe(STRING, [](const StringMessage& msg) {...}); 

Лямбду можно обернуть в std::function, но для этого нужно знать тип лямбды, не потеряв тип входного аргумента. А затем сконвертировать лямбду во что-то универсальное вроде std::function<void (const Message&)>. Но нельзя просто так взять и узнать тип С++ лямбды.

Выяснить тип лямбды

template <typename Function> struct function_traits 	: public function_traits<decltype(&Function::operator())> {};  template <typename ClassType, typename ReturnType, typename... Args> struct function_traits<ReturnType(ClassType::*)(Args...) const> { 	typedef ReturnType (*pointer)(Args...); 	typedef std::function<ReturnType(Args...)> function; }; 

Позаимствовано отсюда. Непонятная, рекурсивно наследующаяся штука, да ещё и с частичной специализацией! Но смысл в том, что каждая лямбда имеет operator(), который и используется для вызова. decltype(&Function::operator()) разворачивает это в тип функции-члена, соответствующей лямбде. Аргументы передаются в частично-специализированный шаблон, где и устанавливаются соответствующие синонимы для типа указателя на функцию и std::function для указателя на функцию.

Код по смыслу аналогичен варианту с делегатами. Усложняется лишь логика работы с лямбдой.

template <typename Function> class Messenger { 	... 	void subscribe(int code, Function func) { 		// Узнаем тип функции с помощью function_traits 		typedef typename function_traits<Function>::function FType;  		// У std::function есть синоним аргумента argument_type (если аргумент единственный)  		typedef typename FType::argument_type Arg; 		 		// Сохраним typeid аргумента 		auto& arg_type = typeid(Arg); 			 		// Проверим, что сообщение пронаследовано от Message 		// Тип Arg является ссылкой. Для проверки типа, ссылку нужно убрать из типа. 		typedef std::remove_reference<Arg>::type ArgNoRef; 		 		// Проверка на наследственность 		static_assert(std::is_base_of<Message, ArgNoRef>::value,  			"Argument type not derived from base Message");  		// Преобразуем лямбду в соответствующий ей указатель на функцию 		auto ptr = to_function_pointer(func);  		// И тут же меняем на нужный тип указателя, который и сохраняем 		auto pass = (void(*) (const Message&)) ptr;  		 		subscribers_.emplace_back(std::move(Subscriber(code, pass, arg_type))); 	} } 

Что внутри to_function_pointer?

Лямбда статически преобразуется к типу указателя на функцию соответствующего типа.

template <typename Function> typename function_traits<Function>::pointer to_function_pointer(Function& lambda) { 	return static_cast<typename function_traits<Function>::pointer>(lambda); } 

На заметку

Стоит заметить что сделать приведение в обратную сторону гораздо проще.

std::function<void (const Message&)>       msg_func = ...; std::function<void (const StringMessage&)> str_func = msg_func; // И всё 

Это логичное поведение, потому что публичное наследование (public inheritance) является реализацией отношения «является» (is a). Конкретно StringMessage является Message. Но не наоборот.

Код отправки почти дословно повторяет разобранный код с делегатами. Весь код с лямбдами.
Вот финал нашего труда. Можно просто зарегистрироваться, послать сообщение и обработать его.

int main() { 	Messenger messenger; 	messenger.subscribe(STRING, [](const StringMessage& msg) { 		std::cout << "Received: " << msg.message << std::endl; 	}); 	messenger.send(StringMessage("Hello, messages!")); 	return 0; } 

Приведу также ссылку на статью с более общей реализацией обратного вызова (callback) для нескольких аргументов.

Просадка производительности

Посмотрим насколько просели по производительности. Возьмём только по одному обработчику для двух мессенджеров, один из которых наш и может принимать любой унаследованный от Message тип. И второй, который умеет принимать только сообщение со строкой StringMessage. Будем посылать одно установленное сообщение много 500 000 000 раз.

Msg: 13955ms  Str:  1176ms Ratio:  12.0 

В 12 раз медленнее. Вся разница уходит на взятие typeid типа аргумента при отправлении на одно сообщение и проверку на совпадение типов. Цифра удручающая, будем помнить о ней, но всё-таки не самая важная. Потому что скорее всего в программе возникнет узкое место не в процессе отправки сообщений, а в их обработке. И в самом крайнем случае можно убрать проверку на тип в релизном режиме, выровняв производительность.
Код замера

О чём я умолчал

Я не затронул вопросы удаления лямбд. В версии с делегатами мы сохраняли у себя указатель на объект и при удалении объекта мы сможем удалить всю информацию о подписчике. Здесь я не вижу других вариантов решения, кроме как поступить также и добавить в метод подписки ещё аргумент указателя на объект.

Итоги

В итоге мы получили простую и вполне удобный прототип системы обмена сообщений. Весь код доступен на GitHub.

ссылка на оригинал статьи http://habrahabr.ru/post/211476/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *