📜 ⬆️ ⬇️

Naive implementation of std :: bind (boost :: bind)

It is probably difficult to find such a C ++ programmer who never used boost :: bind in his code (with the release of c ++ 11 std :: bind). Bind is a template function that returns a wrapper over a callable object (that is, an object that can be called by passing it the required number of arguments in round brackets). Bind allows you to change the signature of the call of such an object, reducing the number of input arguments or swapping some of them. Who cares how this can be implemented using C ++ 11, please under the cat.

Examples of using

Here is the simplest (contrived) example of using bind-a:

int sum( int lhs, int rhs ) { return lhs + rhs; } auto f_sum = std::bind( sum, 3, std::placeholders::_2 ); f_sum( 5, 7 ); // f_sum  10 

In this example, std :: bind takes as its basis a pointer to the sum function and uses lhs as its argument, 3, and rhs for the second argument from the f_sum callable object passed to the input. Thus the result of calling f_sum is 10. Everything is very simple. It is clear that in a similar way, std :: bind can also be used with member functions. The only difference is that in this case, as the first argument, the callable object constructor must pass an object of the corresponding class, which will have the member function called.
')
 struct A { void Print() const { std::cout << "A::Print()" << std::endl; } }; A a; auto f = std::bind(&A::Print, a); f(); // "" A::Print() 

Actually implementation

So, std :: bind is a template function that takes as input a pointer to a callable object and arguments, which can be constants, variables, or placeholders. As part of the new (c ++ 11) standard, this can be written as:

 namespace naive { template<typename Func, typename... BinderArgs> binder<Func, BinderArgs...> bind( Func const & func, BinderArgs &&... args ) { return binder<Func, BinderArgs...>( func, std::forward<BinderArgs>( args )... ); } } 

Here binder is a template class that has a parentheses operator defined. In addition, the binder's task is to store all the arguments passed to it to the input. For ease of implementation, it will do so by value. Note that std :: bind stores arguments also by value, unless you ask it otherwise.

 // ... template<typename Func, typename... BinderArgs> struct binder { binder( Func const & func, BinderArgs &&... binderArgs ) : m_func{ func} , m_args{ std::forward<BinderArgs>(binderArgs)... } {} template<typename... Args> void operator()( Args &&... args ) const { // ... } // ... private: invoker_t m_invoker; Func m_func; args_list<BinderArgs...> m_args; }; // ... 

Here naive :: args_list resembles std :: tuple very remotely. Its task is to store an arbitrary number of arguments of arbitrary types. It is clear that standard containers like std :: vector, list, deque, ... are not suitable for this. The following is the implementation of naive :: args_list.

 // arg        args_list. //    std::size_t ,     //     template<std::size_t, typename T> struct arg { explicit arg( T val ) : value( val ) {} T const value; }; template<typename,typename...> struct args_list_impl; template<std::size_t... Indices, typename... Args> struct args_list_impl<indices<Indices...>, Args...> : arg<Indices, Args>... { template<typename... OtherArgs> args_list_impl( OtherArgs &&... args ) : arg<Indices, Args>( std::forward<OtherArgs>(args) )... {} }; template<typename... Args> struct args_list : args_list_impl< typename make_indices< sizeof...( Args )>::type, Args... > { using base_t = args_list_impl< typename make_indices< sizeof...( Args ) >::type, Args... >; template<typename... OtherArgs> args_list( OtherArgs &&... args ) : base_t( std::forward<OtherArgs>(args)... ) {} }; 

The additional template parameter of the whole type of the naive :: arg structure is needed in order to distinguish several arguments with the same types (the types are the same, the indices are different). By the way, the arguments in binder when calling its operator "parentheses" are passed using args_list (though there is a little trick in case of an empty list).

Moving on. The parentheses operator is defined in the body of the naive :: binder class, which redirects the call to the appropriate invoker ("caller") depending on the callable object passed to the input. There are two possible options: either the “normal” function was transferred and then it should be called like this (free_function_invoker):

 template<typename...Args> void invoke( Func const & func, Args &&... args ) const { return func( std::forward<Args>(args)... ); } 

or a member function of the class was passed and then the call will be like this (member_function_invoker):

 template<typename ObjType, typename...Args> void invoke( Func const & func, ObjType && obj, Args &&... args ) const { return (obj.*func)( std::forward<Args>(args)... ); } 

What type of "caller" to use is determined at the design stage of the binder.

 using invoker_t = conditional_t< std::is_member_function_pointer<Func>::value, member_function_invoker, free_function_invoker >; 

Now the fun part. How to implement the operator "parentheses" in Binder. First, look at the code:

 template<typename Func, typename... BinderArgs> struct binder { // ... template<typename... Args> void operator()( Args &&... args ) const { // need check: sizeof...(Args) should not be less than max placeholder value call_function( make_indices< sizeof...(BinderArgs) >{}, std::forward<Args>(args)... ); } private: template< std::size_t... Indices, typename... Args > void call_function( indices<Indices...> const &, Args &&... args ) const { struct empty_list { empty_list( Args &&... args ) {} }; using args_t = conditional_t< sizeof...(Args) == 0, empty_list, args_list<Args...> >; args_t const argsList{ std::forward<Args>(args)... }; m_invoker.invoke( m_func, take_argument( get_arg<Indices,BinderArgs...>( m_args ), argsList )... ); } // ... }; 

The get_arg <I, BinderArgs ...> function simply returns the ith element from the args_list <BinderArgs ...> list obtained by constructing naive :: binder.

  template<std::size_t I, typename Head, typename... Tail> struct type_at_index { using type = typename type_at_index<I-1, Tail...>::type; }; template<typename Head, typename... Tail> struct type_at_index<0, Head, Tail...> { using type = Head; }; template<std::size_t I, typename... Args> using type_at_index_t = typename type_at_index<I, Args...>::type; template<std::size_t I, typename... Args> type_at_index_t<I, Args...> get_arg( args_list<Args...> const & args ) { arg< I, type_at_index_t<I, Args...> > const & argument = args; return argument.value; }; 

Now consider take_arg. There are two overloads of this function, one of which is necessary for working with placeholders, the other for all other cases. For example, if std :: placeholders :: _ N was passed to the constructor when creating a Binder, then when calling the operator, Binder will substitute std :: placeholders :: _ N instead of the N-th argument passed to the input. In all other cases, the binder will ignore the arguments passed to the “parentheses” operator and will substitute the corresponding values ​​received by it in the constructor.

  template<typename T, typename S> T take_argument( T const & arg, S const & args ) { return arg; } template<typename T, typename... Args, std::size_t I = std::is_placeholder<T>::value, typename = typename std::enable_if< I != 0 >::type > type_at_index_t<I-1, Args...> take_argument( T const & ph, args_list<Args...> const & args ) { return get_arg< I-1, Args... >( args ); } 

That's all. Below are examples of using naive :: bind.

Examples of using
 #include "binder.hpp" #include <iostream> #include <string> void Print( std::string const & msg ) { std::cout << "Print(): " << msg << std::endl; } struct A { void Print( std::string const & msg ) { std::cout << "A::Print(): " << msg << std::endl; } }; int main() { std::string const hello {"hello"}; auto f = naive::bind( &Print, hello ); auto f2 = naive::bind( &Print, std::placeholders::_1 ); f(); f2( hello ); A a; auto f3 = naive::bind( &A::Print, std::placeholders::_2, std::placeholders::_1 ); auto f4 = naive::bind( &A::Print, std::placeholders::_1, hello ); auto f5 = naive::bind( &A::Print, a, std::placeholders::_1 ); auto f6 = naive::bind( &A::Print, a, hello ); f3( hello, a ); f4( a ); f5( hello ); f6(); return 0; } 

It is clear that naive :: bind is a naive (see the name of the post) implementation of std :: bind and does not claim to be included in the standard. Many things could have been implemented differently: for example, instead of args_list, use std :: tuple, differently implement invoker (also called caller in the text), etc. The purpose of the article was to try to figure out how std :: bind works under the hood, to look at its simplest implementation. Hope this worked out. Thanks for attention!

»All sources can be found on Github
»Compile everything g ++ - 6.2

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


All Articles