📜 ⬆️ ⬇️

About liberties in the links or the simplest messaging

Messaging is quite a fundamental thing in Computer Science . We will consider it in an approximation to event-oriented programming ( event-driven ). Terminology, capabilities and implementations may differ: events ( events ), messages ( messages ), signals / slots ( signals / slots ) and callbacks . In general, the essence is that with the arrival of an event, a response is launched.
The messaging system itself in the article served as a demonstration of a free, but acceptable interpretation of links / pointers, simplifying the code. The resulting system is trivial and can only register a handler for a specific message code and send messages with such a code.
Suppose that the handlers are non-trivial, and there are few messages. And that we ourselves generate messages and they do not come to us on the network, for example. In this case, I want to have something more convenient with explicit variable declarations in the message. For example, something like:
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 ... } 

But I want to remove the verification code that is not related to the logic of work, under the hood. Therefore, we replace the pointer to the link, showing that the object just arrives at the handler, and not NULL nullptr. And let the handler immediately accept the required message type.
 void handle_message(const StringMessage& message) { ... } 

How to implement your plans and support other possible message classes?

The idea is simple. During the handler registration, we will know the type of the argument it accepts and write it. And when sending a message, check that the message type matches the type of the handler argument. For each new type of message, look at the base message class 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; }; 


Solution with delegates


Good old delegates work in C ++ 03. One of examples of implementation is described on Habré here . Delegates in this case are only a functional wrapper over member functions. This is a subscription handler.
 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)); } } 

Correctness. As soon as the device of the derived message class becomes less trivial, there is a problem of cutting objects . Upon entering the send method, the object is cut to the base type by moving the passed reference to the base object. The handler does not know about this and uses an invalid link. Let's inform if we meet such an object.
 template <class Base, class Derived> bool is_sliced(const Derived* der) { return (void*) der != (const Base*) der; } 

But it's best to write a compile-time check. The compiler will cut the base type according to the inherited one. And if the pointer increased from 1, then the object was cut.
 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"); 

Unfortunately, the MSVS 2013 compiler does not cope with the compilation of the condition, but gcc-4.8.1 is complete .

Sending a message is easy. We check that the message is not cut off. Run through all handlers. If the codes of the message and the handler match, then check the types for compliance. If everything is the same, then we call the handler.
Posting a message
 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); //  -  } } } } 


It is important not to forget to add a check that MessageDerived indeed inherited from Message . In C ++ 11, there is std::is_base_of in the <type_traits> std::is_base_of . In C ++ 03, the compile time check will have to be written by hand.
The delegate example is simple. Handler class, delegate subscription, and message sending:
 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; } 

Code with delegates
')

C ++ 11


In C ++ 11, lambdas appeared. Our goal is to make the subscription process look very simple:
 messenger.subscribe(STRING, [](const StringMessage& msg) {...}); 

Lambda can be wrapped in std::function , but for this you need to know the type of lambda without losing the type of the input argument. And then convert the lambda into something universal like std::function<void (const Message&)> . But you can not just take and learn the type of C ++ lambda.
Figure out the lambda type
 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; }; 

Borrowed from here . Incomprehensible, recursively inherited thing, so even with partial specialization! But the point is that each lambda has operator() , which is used for the call. decltype(&Function::operator()) expands this to the type of the member function corresponding to the lambda. The arguments are passed to the partially specialized template, where the corresponding synonyms for the type of the function pointer and the std::function for the std::function pointer are set.

The code is similar to the delegates variant. Complicated only the logic of working with lambda.
 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))); } } 

What's inside to_function_pointer?
Lambda is statically converted to a pointer type to a function of the corresponding type.
 template <typename Function> typename function_traits<Function>::pointer to_function_pointer(Function& lambda) { return static_cast<typename function_traits<Function>::pointer>(lambda); } 


On a note
It is worth noting that make the cast in the opposite direction is much easier.
 std::function<void (const Message&)> msg_func = ...; std::function<void (const StringMessage&)> str_func = msg_func; //   

This is a logical behavior because public inheritance is the realization of the is relationship ( is a ). Specifically, StringMessage is a Message . But not vice versa.

The sending code almost literally repeats the parsed code with delegates. All code with lambdas .
Here is the final of our work. You can simply register, send a message and process it.
 int main() { Messenger messenger; messenger.subscribe(STRING, [](const StringMessage& msg) { std::cout << "Received: " << msg.message << std::endl; }); messenger.send(StringMessage("Hello, messages!")); return 0; } 

I will also provide a link to an article with a more general implementation of a callback for several arguments.

Performance drawdown


Let's see how much sagged in performance. We take only one handler for two messengers , one of which is ours and can accept any type inherited from Message . And the second, which can only accept a message with the string StringMessage . We will send one set message many 500,000,000 times.
 Msg: 13955ms Str: 1176ms Ratio: 12.0 

12 times slower. The whole difference is spent on taking the typeid type of the argument when sending to one message and checking for type matching. The figure is depressing, we will remember it, but still not the most important. Because most likely in the program there will be a bottleneck not in the process of sending messages, but in their processing. And in the most extreme case, you can remove the check for the type in the release mode, aligning the performance.
Measurement Code
What did I keep silent about
I did not touch on the removal of lambda. In the delegate version, we kept a pointer to the object, and when we delete the object, we can delete all the information about the subscriber. Here I do not see any other solutions, except how to do the same and add another argument of the object pointer to the subscription method.

Results


As a result, we got a simple and quite convenient prototype of the messaging system. All code is available on GitHub .

Source: https://habr.com/ru/post/211476/


All Articles