Сама система обмена сообщениями в статье послужила демонстрацией вольной, но допустимой интерпретации ссылок/указателей, упрощающей код. Получившаяся система тривиальна и умеет только регистрировать обработчик на определённый код сообщения и посылать сообщения с таким кодом.
Допустим что обработчики нетривиальные, а сообщений немного. И что мы сами генерируем сообщения и они не приходят нам по сети, например. В таком случае хочется иметь что-то более удобное с явными объявлениями переменных в сообщении. Например, нечто подобное:
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))); } }
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/
Добавить комментарий