📜 ⬆️ ⬇️

Simplest C ++ Delegate

logo In C # there are delegates. There are delegates in python. There are delegates in javascript. In Java, there is a closure that performs their role. And in C ++ there are no O_O delegates. Many talented programmers successfully struggle with this shortcoming by developing and using sigslots, boost :: function and other valuable and necessary libraries. Unfortunately, most implementations differ not only in the method of use, but also in the epic complexity of the patterned magic used. In order to study the source code of boost :: function, the hair did not stand on end, I wrote this short article showing how to implement a delegate in C ++ in the simplest and most clumsy way. The described implementation is illustrative, has many drawbacks and can hardly be used in serious projects - but it is as simple as possible and allows you to familiarize yourself with the subject area without disassembling the three-story sigslots templates :).



Why do you need it


')
As practice shows, if a large program consists of a large number of small pieces that are as independent as possible from each other, the easier it is to develop, repair and change. Modern object-oriented programming languages ​​offer us objects as a piece - hence the name. An object is usually an instance of a class that does something needed and useful. You can make the whole program out of one ba-a-a-al-class - 'god object antipattern' - and in a couple of years making any change will turn into a curse of the sixth level. And you can break the program into small classes, as independent as possible from each other classes, and then everyone will be dry and comfortable. But breaking the program in this way raises the second question - how will these classes interact with each other? Here the programmer comes to the aid of decomposition tools provided by the programming language. The simplest means of decomposition in C ++ is to make classes global and directly call their methods. The approach is not without flaws - it becomes increasingly difficult to understand who causes whom over time, and in a couple of years this approach will lead to the same consequences as the use of a single class. An option approved by the Holy Inquisition is the transmission of pointers to classes to those classes with which they need to communicate. Moreover, it is desirable that the pointers are not simple, but for interfaces - then changing and developing the program over time will become much easier.
However, the interfaces are not a silver bullet (some say that such a bullet, like spoons, are not). If an object needs only a few interactions, for example, a button must be notified that it has been clicked, then the implementation of a separate interface for these purposes will take a noticeable number of lines of code. Also, interfaces do not solve the problem when one object needs to notify about several others - creating and maintaining a subscription list based on interfaces is also not the smallest number of lines of code.
In dynamic programming languages, competition to interfaces is delegates. As a rule, a delegate is very similar to a “pointer to a function” in C ++, with the main difference being that a delegate can point to a method of an arbitrary object. In terms of code, the use of a delegate usually looks like this:
delegate example
The same thing with the use of the interface illustrates the need and benefit of delegates:
interface example

What should a C ++ delegate look like?



It should be something that can be associated with an arbitrary class method and then called as a function. In C ++, this can only be done in the form of a class with an overloaded call operator (such classes are usually called functors). In C #, the "+ =" operator is used to associate a delegate and a method, but unfortunately it is impossible in C ++ - the + = operator takes only one parameter, while a pointer to a function is a member of a class in C ++ with two parameters. Therefore, using a C ++ delegate should look something like this:
delegate usage

The simplest implementation for a single argument



Let's try to implement this behavior. In order for a delegate to get a pointer to any method of any class, its own Connect () method must clearly be template. Why not make the delegate template itself? Because then you have to specify a specific class when creating a delegate, and this is contrary to the ability to associate a delegate with any class. Also, the delegate must have an overloaded call operator, also a template one, so that it can be called with the same types of arguments as the method associated with it. So, delegate stub will look like:
delegate skeleton
The Connect () method can be called with a pointer to a method of any class, and the call operator allows the delegate itself to be called with any argument. Now all that needs to be done is to somehow save the pointer to the i_class class and the i_method method so that they can be used in the call operator. Here the plug-in happens a number of times - the delegate himself doesn’t know about T and M, and he shouldn’t know, save them as fields will not work (these are arguments of the template method that can be called for different delegates many times with different classes and methods). What to do? You'll have to turn to a little template magic (believe me, these are spells of the first level compared to those used in boost: function). The only way in C ++ to save template arguments is to create an instance of a template class that will be parameterized with these arguments, and, accordingly, will remember them. Therefore, we need a template class that can remember T and M. And to save a pointer to this class, it must be inherited from an interface that does not have template parameters:
delegate with container
In the first approximation, this is enough so that you can call Connect () for any method of any class and remember the arguments. But remembering is half the battle. The second half is to call the memorized method with the argument passed to us. There is a certain complexity with the call - we saved the pointer to the class method as an IContainer interface - how can we call the class method with an arbitrary type parameter that the user passed to operator ()?
argument pass problem
The easiest way is to remember the passed argument in the container just as we did for the pointer to the class method, pass the container with the argument “inside” m_container, and then use dynamic_cast <> () to “take out” the argument from the container. It sounds scary, but the code is quite simple:
argument pass solution draft
The final problem on the way to a working delegate is to retrieve the argument from the container. In order to do this you need to know the type of the argument. But after all, inside the container that stores the pointer to the class method, we do not know the type of the argument? The type of the argument is not known - but we know the signature of the method, the pointer to which we store. Therefore, all we need to do is extract the type of the argument from the signature. To do this, you must use the trick with a partial specialization of templates, described in my article . It will look like this:
argument pass trick
Actually, everything. The resulting delegate stores a pointer to any method of any class and allows you to call it with the function call syntax.

Implementation for an arbitrary number of arguments



The implementation shown above has one drawback - it works only with methods that have exactly one argument. And what if the method takes no arguments? Or accepts two, and even three arguments? The solution that is used for C ++ in boost, sigslots and Qt is quite simple: we copy the corresponding parts of the code for all supported cases. Usually they do support from zero to four arguments, because if you need to pass more than four arguments when linking two objects, then we have something wrong with the architecture and probably we are trying to repeat the WinAPI feat CreateWindow () O_O. The finished implementation code with support for up to two arguments and an example of use is presented below. I remind you that it is illustrative and greatly simplified. Many checks are missing, variable names are sacrificed in favor of compactness, and so forth, so on. For production, it's better to use something like boost :: function :)

#include <assert.h> //     2- . struct NIL {}; class IArguments { public: virtual ~IArguments() {} }; template< class T1 = NIL, class T2 = NIL > class Arguments : public IArguments { public: Arguments() {} public: Arguments( T1 i_arg1 ) : arg1( i_arg1 ) {} public: Arguments( T1 i_arg1, T2 i_arg2 ) : arg1( i_arg1 ), arg2( i_arg2 ) {} public: T1 arg1; T2 arg2; }; //      . class IContainer { public: virtual void Call( IArguments* ) = 0; }; template< class T, class M > class Container : public IContainer {}; //     . template< class T > class Container< T, void (T::*)(void) > : public IContainer { typedef void (T::*M)(void); public: Container( T* c, M m ) : m_class( c ), m_method( m ) {} private: T* m_class; M m_method; public: void Call( IArguments* i_args ) { (m_class->*m_method)(); } }; //      . template< class T, class A1 > class Container< T, void (T::*)(A1) > : public IContainer { typedef void (T::*M)(A1); typedef Arguments<A1> A; public: Container( T* c, M m ) : m_class( c ), m_method( m ) {} private: T* m_class; M m_method; public: void Call( IArguments* i_args ) { A* a = dynamic_cast< A* >( i_args ); assert( a ); if( a ) (m_class->*m_method)( a->arg1 ); } }; //       template< class T, class A1, class A2 > class Container< T, void (T::*)(A1,A2) > : public IContainer { typedef void (T::*M)(A1,A2); typedef Arguments<A1,A2> A; public: Container( T* c, M m ) : m_class( c ), m_method( m ) {} private: T* m_class; M m_method; public: void Call( IArguments* i_args ) { A* a = dynamic_cast< A* >( i_args ); assert( a ); if( a ) (m_class->*m_method)( a->arg1, a->arg2 ); } }; //  . class Delegate { public: Delegate() : m_container( 0 ) {} ~Delegate() { if( m_container ) delete m_container; } template< class T, class U > void Connect( T* i_class, U i_method ) { if( m_container ) delete m_container; m_container = new Container< T, U >( i_class, i_method ); } void operator()() { m_container->Call( & Arguments<>() ); } template< class T1 > void operator()( T1 i_arg1 ) { m_container->Call( & Arguments< T1 >( i_arg1 ) ); } template< class T1, class T2 > void operator()( T1 i_arg1, T2 i_arg2 ) { m_container->Call( & Arguments< T1, T2 >( i_arg1, i_arg2 ) ); } private: IContainer* m_container; }; class Victim { public: void Foo() {} void Bar( int ) {} }; int main() { Victim test_class; Delegate test_delegate; test_delegate.Connect( & test_class, & Victim::Foo ); test_delegate(); test_delegate.Connect( & test_class, & Victim::Bar ); test_delegate( 10 ); return 0; } 


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


All Articles