こんにちは。田原です。
C++は関数の中で関数を定義できませんがクラスは定義できました。ではテンプレートはできるか?と問うと残念ながらできないようです。しかし、C++14から導入されたジェネリック・ラムダは一種のテンプレートであり、これは関数の中で定義することができます。つまり、現在のところ関数内で定義可能な唯一のテンプレートがジェネリック・ラムダです。今回はジェネリック・ラムダの解説を兼ねて第21回のtupleの動的処理にジェネリック・ラムダを使ってみます。
1.tupleの要素の動的処理の改造
第21回では、tupleの要素を動的に指定して、それをprintする処理を実装しました。
今回は、そのprint処理をprint以外の処理に自由にすげ替えることができるようにします。
そのために「tupleの要素を動的に指定して処理を呼び出す部分」と「処理」を分離します。
1-1.配列化の復習
この後での混乱を少しでも回避するため、第21回のstd::tupleを配列へ展開している部分を復習します。
tupleを動的に処理するために「tupleの各要素をprintする関数へのポインタ」の配列を作りました。そして、その配列の添え字を変数で指定して動的に処理しました。
さて、関数テンプレートはテンプレートなので実体化する前は実体(機械語)がありません。実体がないので当然アドレスもありませんから、関数ポインタを作れません。
さて、第21回のサンプルの該当部分をもう一度見てみましょう。
template<std::size_t i, class tTuple> void printElement(tTuple const& iTuple) { std::cout << std::get<i>(iTuple) << "\n"; } template<class tTuple, std::size_t... indices> void printImpl(tTuple const& iTuple, std::size_t i, std::index_sequence<indices...>) { using Func=void (*)(tTuple const&); Func aFuncArray[]= { (printElement<indices, tTuple>)... }; aFuncArray[i](iTuple); }
関数テンプレートへのポインタの配列を作っているかのように見えます。
しかし、実は関数テンプレートが実体化されたものの配列を作っています。
(printElement<indices, tTuple>)...
にて、テンプレート・パラメータ・パックindicesを展開し、Func型の配列aFuncArrayを初期化しています。
つまり、printElement<indices, tTuple>
が関数ポインタに展開されているというわけです。
この構文は関数テンプレートの実体化です。
tTupleとindicesが指定されてprintImpl()関数テンプレートが実体化された時にはprintElement<indices, tTuple>
も実体化されますので、printElement関数のアドレスが確定し、aFuncArray配列が作られます。
1-2.「処理」を呼び出し側で指定する
1-2-1.関数テンプレートで「処理」を指定する
printElement関数テンプレートは決まった「表示処理」を行いますが、代わりに「処理」を行う関数を渡したいです。
ただし、その処理対象はtupleの各要素ですから要素毎に型が異なります。異なる型に対して同じ形式の処理を行いたい時は関数テンプレートが使えます。
そこで、例えば、次のような関数テンプレートを「処理」として渡せるようにしたいです。
template<typename T> void process(T t) { std::cout << "process(" << t << ")\n"; }
さて、ここで問題があります。
実体化されていないテンプレートはクラスでも関数でもありません。つまり「型」ではないのです。テンプレート・パラメータの型パラメータでは渡せません。ではどうすれば渡せるでしょうか?
- 非型パラメータで渡せるか?
字面から非型パラメータならとも思いますが、非型パラメータは「型ではないもの」全般という意味ではなく「値」を渡すテンプレート・パラメータです。テンプレートを渡せるわけではありません。 -
テンプレート・テンプレート・パラメータで渡せるか?
「テンプレート」を「テンプレート・パラメータ」として渡すことができます。これを「テンプレート・テンプレート・パラメータ」と呼びます。これを使って「関数テンプレート」を渡せればよいのですが、どうも無理なようです。テンプレート・テンプレート・パラメータに渡せるものはクラス・テンプレートだけのようです。
関数テンプレートをテンプレート・テンプレート・パラメータに渡せないのか?
そこそこ調べたのですが、渡せる方法が見つかりませんでした。もし、関数テンプレートを渡すことが可能でその方法をご存知の方がいらっしゃいましたら是非教えて下さい。
1-2-2.関数オブジェクトで「処理」を指定する
どうも関数テンプレートを直接渡すことはできないようです。
他にどんなテンプレートが使えるでしょう?(tupleの各要素の型に対応するため、テンプレートを使うことは必須です。)
- クラス・テンプレートな関数オブジェクト
template<typename T> struct Functor { void operator()(T t) { std::cout << "process(" << t << ")\n"; } };
-
operator()が関数テンプレートな関数オブジェクト
struct Functor { template<typename T> void operator()(T t) { std::cout << "process(" << t << ")\n"; } };
クラス・テンプレートな関数オブジェクトのクラスを「テンプレート・テンプレート・パラメータ」で渡すことができます。しかし、あまりにも複雑ですのでお勧めできません。「operator()が関数テンプレートな関数オブジェクト」の方が簡単です。
そこで、ここでは「operator()が関数テンプレートな関数オブジェクト」を使います。
(といいますか、長々と引っ張りましたが実はこれこそがジェネリック・ラムダです。)
2.operator()が関数テンプレートな関数オブジェクトでtupleの要素を動的処理
動的処理の「処理」を渡すために「operator()が関数テンプレートな関数オブジェクト」を使ってみます。
渡すものは関数オブジェクトですから、「普通」のクラスです。テンプレートではありません。つまり普通のクラスを渡すのと同じ方法で渡せます。(そこはかとなく違和感を感じますが、テンプレートなのはあくまでもメンバ関数です。)
2-1.Element処理
元のprintElementは、指定されたtupleの指定番目の要素を「表示」しました。この「表示」の部分をパラメータtProcess iProcess
で渡してみます。そのために、元のprintElement関数を修正します。(printElementでは名前が可笑しいので、procElementへ変更します。)
template<std::size_t i, class tTuple, typename tProcess> void procElement(tTuple const& iTuple, tProcess iProcess) { iProcess(std::get<i>(iTuple)); }
単純に、tProcess型のiProcessを受け取り、それを関数オブジェクトとみなしてoperator()を呼び出しました。operator()は例えば、次のように定義されていれば呼び出せます。
template<typename T> void operator()(T t);
2-2.入口関数とtupleの展開処理
procElementの配列を作り、指定要素のprocElementを呼び出すprocImplとそれを呼び出すprocは次のようになります。(元のprintImplにtProcess iProcessを追加してprocElementへ中継しただけです。なお、可変長引数の後に引数を指定できませんので、その前に追加しています。)
template<class tTuple, typename tProcess, std::size_t... indices> void procImpl(tTuple const& iTuple, std::size_t i, tProcess iProcess, std::index_sequence<indices...>) { using Func=void (*)(tTuple const&, tProcess iProcess); Func aFuncArray[]= { (procElement<indices, tTuple, tProcess>)... }; aFuncArray[i](iTuple, iProcess); }
そして、動的処理入口関数となるprocを定義します。これはindex_sequenceを型推論で生成するために中継しています。
template<class tTuple, typename tProcess> void proc(tTuple const& iTuple, std::size_t i, tProcess iProcess) { constexpr std::size_t n = std::tuple_size<tTuple>::value; procImpl(iTuple, i, iProcess, std::make_index_sequence<n>{}); }
2-3.「処理」の定義とtupleの動的呼び出し
「処理」を指定する関数オブジェクトのクラスを定義し、これを渡します。
struct Functor { template<typename T> void operator()(T t) { std::cout << "Functor(" << t << ")\n"; } }; int main() { std::tuple<int, std::string, long> aTuple(123, "abc", 987654321); for (std::size_t i=0; i < std::tuple_size<decltype(aTuple)>::value; ++i) { proc(aTuple, i, Functor()); } }
実行するとこのように動作します。予定通りです。
Functor(123) Functor(abc) Functor(987654321)
3.最後にジェネリック・ラムダ
さて、Functorを別途定義しないといけないことが頂けません。しかもメンバ関数がテンプレートなのでローカルに定義でません。短い処理を渡す時にまで関数の外で定義するのは可読性が劣化します。
そこで、C++14で規定されたジェネリック・ラムダの出番です。
既に予想が付いている方もいるかと思いますが、先程のFunctorをラムダ式にしたものが、ジェネリック・ラムダです。つまり、関数テンプレートなoperator()を持つ関数オブジェクトをラムダ式化したものがジェネリック・ラムダです。
書き換えの手順は簡単です。
第22回目で普通の関数オブジェクトをラムダ式にしたのと同様な手順+αで変形できます。
元の関数オブジェクトとなるクラスに対して、皮のstruct クラス名{ }を削除し、void operataor()の部分を[]へ書き換えます。そして、template<typename T>
を削除し、Tをautoへ書き換えます。
(元はクラスですが出来上がったものはクラスではなくオブジェクトです。ご注意下さい。)
[](auto t) { std::cout << "Functor(" << t << ")\n"; } ;
そして、autoに対するconst、volatile、&(参照)、*
(ポインタ)修飾も普通のテンプレートと同様に可能ですし、同じ意味を持ちます。例えば、下記のように記述されていた場合も同様です。
struct Functor { template<typename T> void operator()(T const& t) { std::cout << "Functor(" << t << ")\n"; } }; ↓ [](auto const& t) // tは与えられた実引数へのconst参照となる { std::cout << "Functor(" << t << ")\n"; } ;
ところで、ジェネリック・ラムダは普通のラムダ式と同じく、普通の関数オブジェクトです。テンプレートではありません。ですので、普通に変数に代入できます。
auto functor = [](auto t){ std::cout << "Functor(" << t << ")\n"; };
では、「2-3.「処理」の定義とtupleの動的呼び出し」のFunctorの代わりにジェネリック・ラムダを渡してみます。
int main() { std::tuple<int, std::string, long> aTuple(123, "abc", 987654321); for (std::size_t i=0; i < std::tuple_size<decltype(aTuple)>::value; ++i) { proc(aTuple, i, [](auto elem) { std::cout << "Lambda: " << elem << "\n"; }); } }
Lambda: 123 Lambda: abc Lambda: 987654321
Functorの定義が不要になるのでずいぶんすっきりしますし、短い処理の場合、普通にfor文を書いた時と同じく視線がウロウロしないで済むので有り難いですね。
ジェネリックとテンプレート
C++のテンプレートと似た機能は、C#やJava等の言語ではジェネリックと呼ばれることが多いです。
genericの意味は「汎用」ですから「テンプレート(雛形)」とはちょっと意味が異なりますね。
C++のクラス・テンプレートや関数テンプレートは、実体化して使用します。実体化された結果は通常のクラスや関数です。
それに対して、C#やJavaのジェネリックは実体化が不要です。直接使えます。(便利ですけどパラメータの型を動的に判定して各々に適切な処理を呼び出しますので遅いです。)受け取るパラメータの型が自由になるというだけですからテンプレートと表現すると間違っています。ですので、テンプレートと呼ばずジェネリックと呼ぶのは妥当ですね。C++のテンプレートも汎用ですからジェネリックと呼んでも間違いではないと思います。しかし、テンプレートの方がその仕組を端的に表現しているので好ましく感じます。
さて、何故にジェネリック・ラムダであってテンプレート・ラムダと呼ばれないのか? 既に答えを書いてます。ジェネリック・ラムダは関数オブジェクトであってテンプレートではないからだと思います。
4.まとめ
ジェネリック・ラムダはC++14で導入された機能です。C++14はC++11に比べるとマイナーチェンジがほとんどなのですが、冒頭でも軽く触れているようにジェネリック・ラムダによって初めて関数の中関数テンプレート(正確にはメンバ関数テンプレート)を定義できます。しかも、関数オブジェクトですから、関数テンプレートなのに実体化する前に実引数として関数へ渡せる優れものです。なかなか便利な機能だと思います。
さて、C++11の重要な機能について概ね解説し本格的にネタも尽きてきました。そして、最近少し本業が忙しくなってきています。またTheolizerのC#連携を速く進めたいのですが、別件が忙しくてなかなか進まない状況もあり、これを改善したいです。
そこで、当講座を今回で一旦修了とし、以降不定期に開講する形式にしたいと思います。
今後もC++14やC++17の機能も使っていきたいと思っていますし、それらの便利な機能を把握できたら随時解説したいと考えております。またC++11以前の機能についても解説した方が良いネタを見つけたら時間を取って解説してみたいとも考えていますので、時々是非見に来て頂けると幸いです。
それではお疲れ様でした。また不定期に開講致しますので、今後共よろしくお願い申し上げます。