こんにちは。田原です。
前回、std::tupleの要素全てを静的に枚挙してみました。しかし、全てではなく特定の1つを表示したい場合もあるでしょう。また、どの一つを表示するのかコンパイル時ではなくプログラム起動後に決定したいこともあると思います。switch-case文で処理すれば可能ですが、std::tupleの要素の数が異なる毎にswitch-case文を書必要があり、それは避けたいものですね。そこで、今回はその方法について解説します。
1.まずはswitch-caseで処理してみる
まず最初に思いつくことはswitch-caseで処理することと思いますので、まずはそれでやってみます。
その際、動的処理と静的処理は意外に見分けにくいです。(*1)
(*1)動的処理と静的処理は意外に見分けにくい
const int n0=100; // 静的定数 int n1=100; // 動的点数 extern const int n2; // 動的定数 int array0[n0]; // OK int array1[n1]; // コンパイル・エラー int array2[n2]; // コンパイル・エラー const int n2=100;
入力された数値で処理すれば動的処理(プログラム起動後に決定された値で処理)であることは確実です。そこで、番号を標準入力で受け取り、それに対応するstd::tupleの内容を表示してみます。
また、要素数を越えるまで繰り返すことにします。
#include <iostream> #include <string> #include <tuple> int main() { std::tuple<int, std::string, long> aTuple(123, "abc", 987654321); while(true) { std::size_t i; std::cin >> i; // std::cout << std::get<i>(aTuple) << "\n"; // iがコンパイル時定数ではないのでできない。 switch(i) { case 0: std::cout << std::get<0>(aTuple) << "\n"; // 0はコンパイル時定数なので可能。 break; case 1: std::cout << std::get<1>(aTuple) << "\n"; // 1はコンパイル時定数なので可能。 break; case 2: std::cout << std::get<2>(aTuple) << "\n"; // 2はコンパイル時定数なので可能。 break; default: return 0; } } }
このswitch-case文をaTupleの要素の数が異なる毎に用意する必要がありますね。
関数テンプレートでどうにかできないでしょうか?
#include <iostream> #include <string> #include <tuple> #include <type_traits> template<class tTuple, typename std::enable_if<std::tuple_size<tTuple>::value == 3, std::nullptr_t>::type =nullptr> void print(tTuple const& iTuple, std::size_t i) { switch(i) { case 0: std::cout << std::get<0>(iTuple) << "\n"; // 0はコンパイル時定数なので可能。 break; case 1: std::cout << std::get<1>(iTuple) << "\n"; // 1はコンパイル時定数なので可能。 break; case 2: std::cout << std::get<2>(iTuple) << "\n"; // 2はコンパイル時定数なので可能。 break; } } int main() { std::tuple<int, std::string, long> aTuple(123, "abc", 987654321); while(true) { std::size_t i; std::cin >> i; if (std::tuple_size<decltype(aTuple)>::value <= i) break; print(aTuple, i); } }
う~~ん、当然ですけどやっぱり無理です。case文を自動的に増やせるような構文があればよいのですが、少なくとも現時点ではありませんのでswitch-caseでは無理です。
2.では配列で作ってみます
Swallowテクニックは配列の初期化を使ってました。つまり、std::tupleを静的に展開する際に配列の初期化を使うことが出来るわけです。そして、配列ならインデックスを指定して、動的に要素を指定することもできますね!!
なんかできそうな気がします。前回のindex_sequenceを使って配列を定義し、パラメータでアクセスしたいインデックスを指定してみましょう。
template<class tTuple, std::size_t... indices> void printImpl(tTuple const& iTuple, std::size_t i, std::index_sequence<indices...>) { using Element=要素の型は何?; Element aArray[]= { (indicesを使った何か)... }; // aArray[i]をどうすれば良い? } template<class tTuple> void print(tTuple const& iTuple, std::size_t i { constexpr std::size_t n = std::tuple_size<tTuple>::value; printImpl(iTuple, i std::make_index_sequence<n{}); }
さて、問題は配列の要素の型には何を書けばできるでしょうか?
Swallowテクニックでは要素の型をint型とし、処理した結果を ,(カンマ)演算子を使って飲み込み、各要素にカンマの次に書いた値 0 を放り込んでました。しかし、今回は初期化後に使いたいので、単なる 0 が残っていても意味無いですね。
そこで、この配列を関数ポインタの配列とし、配列の要素を指定してその関数を呼び出してみましょう。
template<class tTuple, std::size_t... indices> void printImpl(tTuple const& iTuple, std::size_t i, std::index_sequence<indices...>) { using Func=どんなシグニチャ?; Func aFuncArray[]= { (indicesを使った関数)... }; std::cout << aFuncArray[i](パラメータは?) << "\n"; }
indicesはstd::get<i>()
へ展開することになる筈ですね。
であれば、Funcに直接std::get<i>()
を指定してみましょう。
template<class tTuple, std::size_t... indices> void printImpl(tTuple const& iTuple, std::size_t i, std::index_sequence<indices...>) { using Func=どんなシグニチャ?; Func aFuncArray[]= { (std::get<indices>)... }; std::cout << aFuncArray[i](iTuple) << "\n"; }
良いところまで来ました。後はFuncの型を決めることができれば完成です。
Funcには関数ポインタの型を定義します。std::get<i>()
のパラメータはタプルのconst参照ですから、Funcのパラメータもタプルのconst参照(この場合、tTuple const&)でOKです。
...
しかし、戻り値の型を決定できません。std::get<i>()
の戻り値は指定したタプルの要素の型です。そして、タプルには異なる型の要素を保存できますので、std::get<i>()
の戻り値の型も i によって異なります。ということは、Func を i によって変えないと行けません。
しかし、aFuncArrayは配列ですから、全て同じ型でないとダメです。がっかりです。
でも概ね答えは見えたと思います。戻り値を受け取ることが目的ではありませんから、関数でラップしてその中で処理すればよいです。
template<std::size_t i, class tTuple> void printElement(tTuple const& iTuple) { std::cout << std::get<i>(iTuple) << "\n"; }
この関数ポインタの型は、void (*)(tTuple const&)
です。先程のprintImpl()関数内でtTupleは変化しませんので、関数ポインタの型も固定です。従ってaFuncArrayの型として指定できます。
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); }
これでタプルの各要素を動的アクセスできるようになりました。ですので動的枚挙も自由自在です。
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) { print(aTuple, i); } }
3.まとめ
今回、std::tupleを例に取って静的なリストを動的にアクセスできるようにしてみました。
ポイントは「静的なリスト=配列の初期化」です。C++では配列を静的に初期化できますね。そして、配列は動的に要素を指定できます。この2つアイデアを応用して、std::tupleの動的アクセスできました。
元ネタをご提供頂いた風露 白山さん、ありがとうございます。std::tupleの動的枚挙は出来ないと思い込んでいましたので、衝撃的でした。
それ程頻繁に使うことはないとは思いますが、std::tupleの展開以外にも使える場面はきっとあると思いますので、ご紹介してみました。
さて、これで前回上げた不満点の1点目を解決できました。そこで、次回からラムダ式を解説します。その例題として不満点の2点目である「枚挙処理とその中で行いたい処理は別物ですからできれば切り離したい」をターゲットにしたいと思います。これはC++14で導入されたジェネリック・ラムダを使うことで解決できますので、次回から数回でジェネリック・ラムダまでを解説します。お楽しみに。