可変長テンプレート。 タプル、開梱など

この投稿では、可変数のパラメーターを持つテンプレートについて説明します。 例として、 タプルクラスの最も単純な実装を示します。 また、 タプルをアンパックし、そこに格納されている値を関数の引数として置き換えることについても説明します。 最後に、上記の手法を使用して遅延関数の実行を実装する例を示します。これは、たとえば、他の言語のfinallyブロックの類似物として使用できます。



理論



可変数のパラメーターを持つテンプレート(可変長テンプレート )は、いわゆるパラメーターパックを受け入れる関数またはクラスのテンプレートです。 テンプレートを宣言すると、次のようになります



template<typename… Args> struct some_type;
      
      





このようなレコードは、テンプレートが引数として0個以上の型を受け入れることができることを意味します。 テンプレートの本文では、使用構文がわずかに異なります。



 template<typename… Args> //  void foo(Args… args); // 
      
      





foo(1,2.3、“ abcd”)の呼び出しfoo <int、double、const char *> (1、2.3、“ abcd”)でインスタンス化されます。 パラメーターパックには多くの興味深いプロパティがあります(たとえば、lambdキャプチャシートやbrace-init-listsで使用できます )が、2つのプロパティについて詳しく説明したいと思います。



1.可変個引数は、関数呼び出しの引数として使用したり、キャスト操作を適用したりできます。 同時に、省略記号の位置に応じて表示されます。つまり、省略記号に直接隣接する式が表示されます。 理解できないように聞こえますが、例を挙げれば、すべてが明らかになると思います。



 template<typename T> T bar(T t) {/*...*/} template<typename... Args> void foo(Args... args) { //... } template<typename... Args> void foo2(Args... args) { foo(bar(args)...); }
      
      







この例では、関数foo2では、省略記号はbar()の呼び出しの後にあるため、最初にargsの各値に対してbar()関数が呼び出され、 bar()によって返された値が引数としてfoo()に入ります。

さらにいくつかの例。



 (const args&..) // -> (const T1& arg1, const T2& arg2, ...) ((f(args) + g(args))...) // -> (f(arg1) + g(arg1), f(arg2) + g(arg2), ...) (f(args...) + g(args...)) // -> (f(arg1, arg2,...) + g(arg1, arg2, ...)) (std::make_tuple(std::forward<Args>(args)...)) // -> (std::make_tuple(std::forward<T1>(arg1), std::forward<T2>(arg2), ...))
      
      







2.パック内のパラメーターの数は、 sizeof演算子を使用して取得できます...



 template<typename... Args> void foo(Args... args) { std::cout << sizeof...(args) << std::endl; } foo(1, 2.3) // 2
      
      







タプル



Tupleクラスは興味深いです。 可変長テンプレートを使用して補助関数を作成するので( 変数なしでも可能です)、 タプルは再帰的なデータ構造であり、別の機能的な世界からの異質なものです(hello Haskell)。これは、多用途のC ++の可能性を再び示しています。

そのようなクラスの簡単な実装を膝の上にスケッチしますが、それでも変数テンプレートを操作するための基本的なテクニックを示しています -パラメーターパックの「頭をかむ」ことと、機能言語で広く使われている「テール」の再帰的処理です。

だから。

基本クラステンプレートはインスタンス化されないため、ボディはありません。



  template<typename... Args> struct tuple;
      
      







テンプレートの主な専門化。 ここでは、コンストラクターで渡されたパラメータータイプの「ヘッド」と引数の「ヘッド」を分離します。 この引数を現在のクラスに保存し、残りは基本クラスによって再帰的に実行されます。 「自分自身」を基本型にキャストすることにより、基本クラスのデータにアクセスできます。



  template<typename Head, typename... Tail> struct tuple<Head, Tail...> : tuple<Tail...> { tuple(Head h, Tail... tail) : tuple<Tail...>(tail...), head_(h) {} typedef tuple<Tail...> base_type; typedef Head value_type; base_type& base = static_cast<base_type&>(*this); Head head_; };
      
      







最後の仕上げは(これも関数型言語になじみがあります)、再帰の「ボトム」を専門にすることです。



  template<> struct tuple<> {};
      
      







一般に、必要な最小値はすでに記述されています。 次のようにクラスを使用できます。



  tuple<int, double, int> t(12, 2.34, 89); std::cout << t.head_ << " " << t.base.head_ << " " << t.base.base.head_ << std::endl;
      
      







ただし、必要な要素に到達するために.baseを書き込む必要がある回数を手動でカウントすることはあまり便利ではないため、 get()関数テンプレートは標準ライブラリに書き込まれ、 タプルクラスのオブジェクトのN番目の要素の内容を取得できます。 関数の部分的な特殊化の禁止を回避するために、関数を構造体にラップする必要があります。 この基本的なテンプレートには、要素タイプと実際にはこの要素を取得する機能の両方で、「頭をかむ」鈍いから、インデックス値が1少ない次のゲッタータイプにリダイレクトすることもあります。



  template<int I, typename Head, typename... Args> struct getter { typedef typename getter<I-1, Args...>::return_type return_type; static return_type get(tuple<Head, Args...> t) { return getter<I-1, Args...>::get(t); } };
      
      







そして、再帰の最下部に来て初めて、最初の実際のアクションを実行できます。 今回は、ダミーから戻り値の型を取得し、そこから取得した値を返します。



  template<typename Head, typename... Args> struct getter<0, Head, Args...> { typedef typename tuple<Head, Args...>::value_type return_type; static return_type get(tuple<Head, Args...> t) { return t.head_; } };
      
      







さて、通常受け入れられているように、構造テンプレートのパラメーターを手動で記述する必要をなくす小さなヘルパー関数が作成されています。



  template<int I, typename Head, typename... Args> typename getter<I, Head, Args...>::return_type get(tuple<Head, Args...> t) { return getter<I, Head, Args...>::get(t); }
      
      







この関数を使用します。



  test::tuple<int, double, int> t(12, 2.34, 89); std::cout << t.head_ << " " << t.base.head_ << " " << t.base.base.head_ << std::endl; std::cout << get<0>(t) << " " << get<1>(t) << “ “ << get<2>(t) << std::endl;
      
      







開梱



タプルをC ++でアンパック! よりクールなものは何ですか?) この機能は、Pythonの作成者にとって非常に重要であると思われたため、この操作をサポートするために言語に特別な構文を導入しました。 これで、C ++で使用できます。 さまざまな方法で実装できます(少なくとも外部では、原則自体はどこでも同じです)が、ここでは、私の意見で最も簡単な解決策を示します。 さらに、ダミーの要素を取得するゲッター 'aを実装するときに上で見たものに似ています。 ここでは、上記の理論で説明されているプロパティ番号1が役立ちます。 unpack関数は次のようになります



 template<typename F, typename Tuple, int… N> auto call(F f, Tuple&& t) { return f(std::get<N>(std::forward<Tuple>(t))...); }
      
      







覚えているように

 f(std::get<N>(std::forward<Tuple>(t))...);
      
      





開梱する

  f(std::get<N1>(std::forward<Tuple>(t)), std::get<N2>(std::forward<Tuple>(t)), ...)
      
      







しかし、1つの問題があります。つまり、そのような関数では、テンプレートのすべてのint引数を手動で指定し、それらを正しく(正しい順序と量で)指定する必要があります。 このプロセスを自動化できれば非常に良いでしょう。 これを行うには、鈍い方法から要素を抽出するのと同様の方法で行動します。



 template<typename F, typename Tuple, bool Enough, int TotalArgs, int... N> struct call_impl { auto static call(F f, Tuple&& t) { return call_impl<F, Tuple, TotalArgs == 1 + sizeof...(N), TotalArgs, N..., sizeof...(N) >::call(f, std::forward<Tuple>(t)); } };
      
      







ここで、それは私には思える、それはより詳細に説明する価値がある。 テンプレートオプションから始めましょう。 FTupleでは、すべてが明確だと思います。 最初は呼び出し可能なオブジェクトを担当し、2番目は実際にはtuplを担当し、そこからオブジェクトを取得し、呼び出しの引数としてcallable 'yをオフにします。 次は、ブールパラメータEnoughです。 十分なintパラメーターが... Nに既に蓄積されているかどうかを示し、それによってテンプレートをさらに特殊化します。 最後に、 TotalArgs-鈍いサイズに等しい値。 呼び出し関数では、以前と同様に、呼び出しをテンプレートの次のインスタンス化に再帰的にリダイレクトします。

この場合、最初の呼び出しでは、タイプは
 call_impl<F, Tuple, TotalArgs == 1, TotalArgs, 0> // (N… - , sizeof...(N) = 0)
      
      



第二に
 call_impl<F, Tuple, TotalArgs == 2, TotalArgs, 0, 1> // (N… =0, sizeof...(N) = 1)
      
      



など それがまさに私たちが必要とするものです。



最後に、実際のアクションが実行される特殊化が必要です。関数は必要な引数で最終的に呼び出されます。 この専門分野は次のとおりです



 template<typename F, typename Tuple, int TotalArgs, int... N> struct call_impl<F, Tuple, true, TotalArgs, N...> { auto static call(F f, Tuple&& t) { return f(std::get<N>(std::forward<Tuple>(t))...); } };
      
      







また、補助機能は損傷しません。



 template<typename F, typename Tuple> auto call(F f, Tuple&& t) { typedef typename std::decay<Tuple>::type type; return call_impl<F, Tuple, 0 == std::tuple_size<type>::value, std::tuple_size<type>::value >::call(f, std::forward<Tuple>(t)); }
      
      







ここでは、すべてが透明だと思います。

これは次のように使用できます。



 int foo(int i, double d) { std::cout << "foo: " << i << " " << d << std::endl; return i; } std::tuple<int, double> t1(1, 2.3); std::cout << call(foo, t1) << std::endl;
      
      







延期する



上記の手法を使用すると、遅延した保留中の計算を整理できます。 そのような計算のプライベートな例として、関数の終了方法、内部の条件付き構築、または例外が発生したかどうかに関係なく、何らかの機能を実行する必要がある状況を考えます。 この動作は、PythonおよびJavaのfinallyブロックに似ています。たとえば、Goには、上記の動作を提供するdeferステートメントがあります。

C ++の他の多くのものと同様に、この問題はさまざまな方法で解決できるようにすぐに予約したいです。たとえば、 std :: bindまたは引数を収集して別のラムダを返すラムダなどを使用します しかし、 呼び出し可能なオブジェクトのストレージも必要であり、必要な引数は非常に適切です。

実際、私たちがすでに知っていることを知っていれば、実装は簡単です。



 template<typename F, typename... Args> struct defer { defer(F f, Args&&... args) : f_(f), args_(std::make_tuple(std::forward<Args>(args)...)) {} F f_; std::tuple<Args...> args_; ~defer() { try { call(f_, args_); } catch(...) {} } };
      
      







いつものように、補助機能



 template<typename F, typename... Args> defer<F, Args...> make_deferred(F f, Args&&... args) { return defer<F, Args...>(f, std::forward<Args>(args)...); }
      
      







そして使用する



 auto d = make_deferred(foo, 1 ,2);
      
      






All Articles