Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

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

Anna | 24.06.2014 | нет комментариев
Обмен сообщениями довольно фундаментальная вещь в науке 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) для нескольких доводов.

Просадка продуктивности

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

Msg: 13955ms 
Str:  1176ms
Ratio:  12.0

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

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

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

Выводы

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

Источник: programmingmaster.ru

Оставить комментарий
БАЗА ЗНАНИЙ
СЛУЧАЙНАЯ СТАТЬЯ
СЛУЧАЙНЫЙ БЛОГ
СЛУЧАЙНЫЙ МОД
СЛУЧАЙНЫЙ СКИН
НОВЫЕ МОДЫ
НОВЫЕ СКИНЫ
НАКОПЛЕННЫЙ ОПЫТ
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB