std :: bindの単純な実装(boost :: bind)

コードでboost :: bind(c ++ 11 std :: bindを使用)を使用したことがないC ++プログラマを見つけるのはおそらく難しいでしょう。 バインドは、呼び出し可能なオブジェクト(つまり、必要な数の引数をかっこで渡すことによって呼び出すことができるオブジェクト)のラッパーを返すテンプレート関数です。 バインドを使用すると、入力引数の数を減らすか、それらの一部を交換することにより、そのようなオブジェクトへの呼び出しの署名を変更できます。 catの下で、C ++ 11を使用してこれをどのように実装できるかを気にする人



使用例



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
      
      





この例では、std :: bindはその基礎としてsum関数へのポインターを取り、lhsは引数として3を使用し、rhsとして、入力に渡されるf_sum呼び出し可能オブジェクトからの2番目の引数を使用します。 したがって、f_sumを呼び出した結果は10になります。すべてが非常に単純です。 明らかに、std :: bindは、同じようにメンバー関数で使用できます。 唯一の違いは、この場合、呼び出し可能オブジェクトのコンストラクターへの最初の引数として、メンバー関数と呼ばれる対応するクラスのオブジェクトを渡す必要があることです。



 struct A { void Print() const { std::cout << "A::Print()" << std::endl; } }; A a; auto f = std::bind(&A::Print, a); f(); // "" A::Print()
      
      





実際に実装



そのため、std :: bindは、呼び出し可能なオブジェクトへの入力ポインターと引数(定数、変数、またはプレースホルダー)を受け取るテンプレート関数です。 新しい(c ++ 11)標準のフレームワークでは、これは次のように記述できます。



 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 )... ); } }
      
      





ここで、バインダーは、括弧演算子が定義されているテンプレートクラスです。 さらに、バインダーのタスクは、入力として渡されたすべての引数をそれ自体に保存することです。 実装を容易にするために、値によって行われます。 特に指定がない限り、std :: bindは値によって引数も保存することに注意してください。



 // ... 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; }; // ...
      
      





ここで、ナイーブ:: args_listは、std :: tupleを非常に漠然と連想させます。 そのタスクは、任意の型の引数を任意の数だけ格納することです。 std :: vector、list、dequeなどの標準コンテナがこれに適していないことは明らかです。 以下は、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)... ) {} };
      
      





構造型naive :: arg全体の追加のテンプレートパラメータは、同じ型の複数の引数を区別するために必要です(型は同じで、インデックスは異なります)。 ちなみに、かっこ演算子を呼び出すときのバインダへの引数は、args_listを使用して渡されます(ただし、空のリストがある場合には少しトリックがあります)。



続けて。 括弧演算子は、ナイーブ::バインダークラスの本体で定義され、入力に渡された呼び出し可能オブジェクトに応じて、呼び出しを対応する呼び出し側(「呼び出し側」)にリダイレクトします。 ここでは2つのオプションが可能です。「通常の」関数が転送された後、次のように呼び出す必要があります(free_function_invoker):



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





クラスのメンバー関数が転送された後、呼び出しは次のようになります(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)... ); }
      
      





どのタイプの「呼び出し元」を使用するかは、バインダーを作成する段階で決定されます。



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





今から楽しい部分です。 バインダーに演算子「括弧」を実装する方法。 まず、コードを見てください:



 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 )... ); } // ... };
      
      





get_arg <I、BinderArgs ...>関数は、単純な::バインダーを作成して取得したargs_list <BinderArgs ...>リストからi番目の要素を単に返します。



  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; };
      
      





次に、take_argを検討します。 この関数には2つのオーバーロードがあり、1つはプレースホルダーを操作するために必要であり、もう1つは他のすべての場合に必要です。 たとえば、バインダーの作成時にstd ::プレースホルダー:: _ Nがコンストラクターに渡された場合、オペレーターが呼び出されると、バインダーはオペレーターの入力のN番目の引数をstd ::プレースホルダー:: _ Nに置き換えます。 他のすべての場合、バインダーは括弧演算子に渡された引数を無視し、コンストラクターで受け取った対応する値を置き換えます。



  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 ); }
      
      





それだけです。 以下は、naive :: bindの使用例です。



使用例
 #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; }
      
      





naive :: bindはstd :: bindのナイーブ(投稿タイトルを参照)実装であり、標準に含まれていると主張していないことは明らかです。 多くのことを異なる方法で実装できます。たとえば、args_listの代わりにstd :: tupleを使用し、invokerを異なる方法で実装します(テキストでは「呼び出し元」とも呼ばれます)。 この記事の目的は、std :: bindが内部でどのように構築されるかを理解し、その最も単純な実装を見ることでした。 それがうまくいったことを願っています。 ご清聴ありがとうございました!



»すべてのソースはGithubにあります

»すべてがコンパイルされましたg ++-6.2



All Articles