こんにちは。田原です。
可変長引数テンプレートとして関数テンプレートが良く使われるので、前回は関数テンプレートで解説しました。クラス・テンプレートも可変長引数テンプレートとして使えますので、今回はstd::tupleを使って可変長引数のクラス・テンプレートを解説します。
1.std::tupleとは
これもC++11でSTLに新たに加わったクラス・テンプレートです。
実は私もあまり使ったことはないのですが、考え方としてはメンバ変数に名前を付けずに済むのでお手軽に使える構造体のようなものです。
例えば、int型とshort型とstd::string型の場合、次のようにして定義します。(比較のため構造体の場合も書いてみました。)
#include <iostream> #include <tuple> int main() { std::cout << "--- struct ---\n"; struct { int mInt; short mshort; std::string mString; } aStruct{123, 456, "hello"}; std::cout << "size=" << sizeof(aStruct) << "\n"; std::cout << aStruct.mInt << "\n"; std::cout << aStruct.mshort << "\n"; std::cout << aStruct.mString << "\n"; aStruct.mString = "world!!"; std::cout << aStruct.mString << "\n"; std::cout << "\n--- std::tuple ---\n"; std::tuple<int, short, std::string> aStdTuple(123, 456, "hello"); std::cout << "size=" << sizeof(aStdTuple) << "\n"; std::cout << std::get<0>(aStdTuple) << std::endl; std::cout << std::get<1>(aStdTuple) << std::endl; std::cout << std::get<2>(aStdTuple) << std::endl; std::get<2>(aStdTuple) = "world!!"; std::cout << std::get<2>(aStdTuple) << "\n"; }
--- struct --- size=40 123 456 hello world!! --- std::tuple --- size=40 123 456 hello world!!
このように、構造体との大きな相違は、メンバ変数に名前が付いていないことです。
そのため、メンバ変数にはグローバルな関数テンプレートstd::get<index>()
を使ってアクセスします。
インデックス番号でアクセスするということはforループでアクセスできることを期待する人もいるかもしれません。しかし、インデックス番号はテンプレート・パラメータで与えます。テンプレート・パラメータはコンパイル時に決まっている必要があるので、forループで作ったものは使えません。
auto aTuple = std::tuple<int, short, std::string>{123, 456, "hello"}; for (std::size_t i=0; i < 3; ++i) { // error: the value of 'i' is not usable in a constant expression std::cout << std::get<i>(aTubple) << "\n"; }
std::get<0>()
はint型、std::get<2>()
はstd::string型なのですから、std::cout <<
で呼び出すoperator<<
は型によってオーバーロードされた異なる関数を呼ぶ必要があります。
つまり、上記コードを動作させるためには、ループの度に異なる実引数をスタックに積み、異なる関数を呼ばないと行けないので、同じ機械語コードでは対応できないのです。
このようにメンバ・アクセスが面倒なので「お手軽な構造体」としてのstd::tupleは、あまり便利なものではないと個人的には感じます。またメンバ変数に名前でアクセスできないのでスコープの広い変数に使うのは如何なものかと感じます。
なので、これだけならあまり使わないですが、実は結構便利な機能がありますので、後述します。
2.簡易tupleを作ってみる
なんかかなり話がそれてしまいました。今回の本題は可変長引数のクラス・テンプレートの展開方法の例示ですので、本題に戻ります。
さて、std::tupleは任意の数の任意の型のデータを保持できます。それを実現するために、可変長引数のクラス・テンプレートで定義されています。
template <class... Types> class tuple;
この記事を書くにあたって、実際のstd::tupleの定義をざっと見てみました。処理系(gcc, VC++)によって異なりますし、予想外に複雑でした。
ですが、ここでは可変長引数のクラス・テンプレートの定義例として説明しますので、お手軽構造体の機能だけ抜き出し、かつ、できるだけ簡単に書いてみます。(std::tupleになるべく近づけたつもりですが、それなりに異なる筈です。constなtupleやムーブなどなど考慮していませんし。)
template<typename... tTypes> class tuple { public: tuple() { } }; template<typename tFirst, typename... tRest> class tuple<tFirst, tRest...> : public tuple<tRest...> { template<std::size_t N, typename... tTypes> friend struct get_helper; tFirst mMember; public: tuple(tFirst const& iFirst, tRest const&... iRest) : tuple<tRest...>(iRest...), mMember(iFirst) { } };
プライマリー・テンプレートはテンプレート・パラメータ・パックtTypesが空の時に使われます。
部分特殊化テンプレートはそれ以外の時に使われ、先頭の型をmMember変数として定義し、残りにてtuple型を実体化して基底クラスにしています。これにより、テンプレート・パラメータ・パックを再帰的に処理してmMemberを次々と定義しています。
なお、基底クラスで次々とmMemberを定義するため、派生クラスでは覆い隠されて見えなくなります。とはいえどうせprivateなので外からはアクセスできません。
ですので、当たり前ですがget<index>()
も定義しないと何の役にも立たないです。
// get_helper プライマリー・テンプレート(形だけ。実体化されることはない) template<std::size_t N, typename... tTypes> struct get_helper { }; // 再帰の最後となるget_helper の部分特殊化 template<typename tFirst, typename... tRest> struct get_helper<0, tFirst, tRest...> { typedef tFirst type; static type& get(tuple<tFirst, tRest...>& iTuple) { return iTuple.mMember; } }; // 再帰を継続するget_helper の部分特殊化 template<std::size_t N, typename tFirst, typename... tRest> struct get_helper<N, tFirst, tRest...> { typedef typename get_helper<N-1, tRest...>::type type; static type& get(tuple<tFirst, tRest...>& iTuple) { return get_helper<N-1, tRest...>::get(iTuple); } }; // get<N>()関数 template<std::size_t N, typename... tTypes> typename get_helper<N, tTypes...>::type& get(tuple<tTypes...>& iTuple) { return get_helper<N, tTypes...>::get(iTuple); }
2018年2月2日:いなむのみたまさんに頂いたコメントからヒントを得て修正しています。
元はstd::conditionalを使っていたため、条件が成立しない方のtypeも実体化されます。その結果、get_helperのプライマリー・テンプレートが実体化され typename get_helper<N, tRest…>::type の type が void となるのでget関数の戻り値の型 get_helper<N, tTypes…>::type& のtypeがvoidとして実体化されます。void&はC++では許されていないため、エラーになります。修正前のサンプルプログラムでは、この対処のため参照ではなくポインタを使いました。その結果、参照のtupleでエラーになってしまったようです。テンプレートの奥は深いです。
修正前のプログラムをWandboxで見てみる。
2-1.get()関数テンプレートのテンプレート・パラメータについて
テンプレート・パラメータとして欲しいメンバのインデックス番号と、ターゲットのタプルのテンプレート・パラメータ・パックを受け取っています。
関数パラメータとして、そのテンプレート・パラメータ・パックで実体化されるtupleへの参照を受け取ります。都合の良いことに、このtupleを渡すとテンプレート・パラメータ・パックを型推論してくれます。ありがたい仕組みです。
2-2.get()関数テンプレートの戻り値の型について
戻り値の型を決めるために、get_helperクラス・テンプレートを使ってます。
インデックス番号 N を1つ減らす毎に先頭の要素を取り除いた get_helperの type で自身のtypeをtypedefしています。インデックス番号 0 のget_helperはその時の先頭の要素の型を type としてtypedefします。その結果、先頭を0として指定したインデックス番号と同じ位置の型が type としてtypedefされます。(いなむのみたまさんのコメントにあるように C++14を使えば decltype(auto)にて更に簡潔に記述できます。)
なお、指定要素への代入もできるようにするため、要素への参照を返しています。
2-3.get_helperクラス・テンプレートのstaticなget()メンバ関数について
get_helperは N の値を1つ減らす度に、元のパラメータ・リストから先頭の要素を取り除いたパラメータ・リストで実体化されます。Nが0になった時のパラメータ・リストはちょうど、欲しい要素が先頭になる tuple のパラメータ・リストと同じです。その時のget_helperのget()は正にその tuple への参照を受け取ります。
つまり、get_helperのget()関数は N を減らしながら次々と呼ばれ、最後に N==0になった時、mMemberを返します。それが次々と呼び出し元へ返却され、最終的に元のget
Wandboxで確認する。
(いなむのみたまさんの参照のtuple例も追加しています。)
3.std::tupleの便利な機能
std::tupleには実はかなり強力な機能があります。(簡易版の上記tupleは実装していません。)
複数の値を使って大小比較したい時って時々あります。プログラムしていて良くでてくるのがバージョン番号の比較ですね。例えば、「2.3.5以上なら◯◯し、そうでないなら△△する」みたいな時です。
struct { short major; short minor; short maintenance; };
のように定義することもあるでしょう。
この時、「2.3.5以上なら」という条件判断は意外に面倒です。
if ((2 < version.major) || ((2 == version.major) && (3 < version.minor)) || ((2 == version.major) && (3 == version.minor) && (5 <= version.maintenace))) { std::cout << "New Version!!\n"; } else { std::cout << "Old Version!!\n"; }
こんな時、std::tupleを使うと楽できます。
std::tupleには比較演算子がオーバーロードされてます。これは、上記のような面倒な段階的な比較をoperator<
だけを使って行ってくれます。なので、std::tupleの全てのメンバの型についてoperator<
が定義されていたら、適切に比較処理してくれます。
#include <iostream> #include <tuple> typedef std::tuple<short, short, short> Version; int main() { Version version{2, 3, 10}; if (Version{2, 3, 5} <= version) { std::cout << "New Version!!\n"; } else { std::cout << "Old Version!!\n"; } }
これはVersionの型をstd::tupleへ変更してしまいましたが、構造体のままでも有用です。
Versionは単なる構造体ですから、大小比較するためには、自分で比較演算子を定義する必要があります。
std::tieを使って元の構造体の各メンバへの参照を持つstd::tuple化することで、直感的で実にスマートに定義できます。
#include <iostream> #include <tuple> struct Version { short major; short minor; short maintenace; }; bool operator<=(Version const& iLhs, Version const& iRhs) { return std::tie(iLhs.major, iLhs.minor, iLhs.maintenace) <= std::tie(iRhs.major, iRhs.minor, iRhs.maintenace); } int main() { Version version{2, 3, 10}; if (Version{2, 3, 5} <= version) { std::cout << "New Version!!\n"; } else { std::cout << "Old Version!!\n"; } }
このケースでは、std::tie(iLhs.major, iLhs.minor, iLhs.maintenace)
は、std::tuple<short&, short&, short&>
型を返します。そして、同じstd::tuple<>
同士には比較演算子が定義されているので簡単に再定義できるわけです。
なお、std::tupleですから、当然、各メンバの型は異なっていても構いません。
実はこの機能は、yohhoyの日記のtieを用いた比較演算子の実装をみて知りました。
詳しいことはこのyohhoyさんの日記を参照ください。yohhyさんはC++の標準規格について本当に詳しい方です。
4.まとめ
今回は、std::tupleを例にとって、可変長引数のクラス・テンプレートのテンプレート・パラメータ・パックの展開例を解説してみました。
関数テンプレートは関数を作りますが、関数は実行時に処理時間を消費し、かつ、記憶領域も必要とする「一時オブジェクト」や「変数」を生成したい時に使うものですね。
そして、クラス・テンプレートは「型」を生成する時に使います。「型」自体はコンパイラへの命令に過ぎず、メモリが割り当てられません。「型」のインスタンスを作る時に初めてメモリが割り当てられるのです。インスタンスを作らない限り、実行時に処理時間やメモリを全く消費しません。
私も明確な言葉で説明出来るほどは理解できていないのですが、これらの間には意外に本質的な差異があるようです。この辺の情報処理の本質的な「何か」に触れることができるのが、C++の大きな特長ではないかと感じる今日此の頃です。
さて、次回も可変長引数テンプレートの話題を追いかけます。テンプレート・パラメータ・パックの展開方法や加工方法についてもう少し解説してみます。お楽しみに。
2.簡易tupleを作ってみる の中ほど。「これいにより」とタイポを発見。
C++14時代なのですから、簡易なtupleを作るというのならgetは普通に線形に再帰して、
戻り値型は型推論に任せれば良いのではと思いました。
[参考] https://wandbox.org/permlink/LBnsseoGBUs1ZabL
あと、参照のtuple作ったらコンパイルすら通りませんよ。
https://wandbox.org/permlink/eTnMEtz7oT0Ca89R
おお!! いなむのみたまさん、コメントありがとうございます。
typoお恥ずかしい。
getを普通に再帰すれば良いとのこと、なるほど確かにその通りでした。
std::conditionalがちょっと嫌な予感はしてましたが、参照のtupleで不具合になってしまったようです。
目標基底クラスへのキャストではなく、普通に再帰すればうまくできたようなので、記事を修正します。
ところで形としてはC++11を当ブログの標準にしてます。C++14ももちろん解説した方が良いことは分かっているのですが、なかなか追いつけていないという問題も...
C++11でも戻り値型の後置宣言でdecltype(auto)と同様のことが行なえます。
同じ式を2回書くのでアホらしいですが。
ついでにtuple_get自体をクラステンプレートにしてenable_ifをやめました。
[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ https://wandbox.org/permlink/GSKPDhvCw5tRDY0d
なるほど。メンバ変数としてtailを定義しdecltypeを上手に使えば、鬱陶しい tFirst をなくせるんですね!
無駄なくスッキリしていて素晴らしいです。
ところで、typoがあるようです。19行目のdecltypeは、
誤>decltype((tuple_get<-1>::get_impl(t)))
正>decltype((tuple_get<I-1>::get_impl(t.tail)))
ですね。