C++11 の Tuple で複数個の型をまとめて扱う - C++ プログラミング
PROGRAM
.auto-style1 {
font-size: 12px;
}
C++11 の Tuple で複数個の型をまとめて扱う
C++11 には、複数個の型をまとめて 1 つにする Tuple(タプル)が規定されました。
これまでは関数で複数の値を返す必要があったときに、引数に参照で取ったり、戻り値のために構造体やクラスを別途定義する必要がありました。ただ、この方法だと引数が IN なのか OUT なのかが判りにくかったり、たった数回使うために構造体を用意するのも手間でした。
新しい Tuple を使うと、必要なときに必要な数の変数をまとめて扱うことができるようになります。
ここではその Tuple の使い方について見て行きます。
Tuple の定義
Tuple は、次のように定義します。
std::tuple<int, CMyClass, double> tupleValue;
このように std::tuple に続けて <> の中でそれが持つデータ型を必要な数だけカンマ区切りで指定します。
これが Tuple データ型になるので、最後に変数名を添えれば、Tuple 型の変数が定義できます。
Tuple の操作
Tuple に格納されている値は std::get<index> 関数を使って取り出します。
int value1 = std::get<0>(tupleValue);
CMyClass value2 = std::get<1>(tupleValue);
double value3 = std::get<2>(tupleValue);
std::get 関数の <> には、Tuple の中の何番目のデータを取得したいかを 0 から始まるインデックスで指定します。
続けて、引数として操作対象の Tuple 変数を指定します。
このとき、std::get<index> 関数で取得できるのは、Tuple 内のそのデータの参照になります。
そのため、取得した参照に値を書き込むことによって、Tuple の値を変更することも可能です。
std::get<0>(tupleValue) = 10;
std::get<1>(tupleValue) = instance;
std::get<2>(tupleValue) = 15.5;
初期値をいっきに揃えたいのであれば、std::get<index> 関数を使わなくても、設定する値を順番に Tuple 型のコンストラクタに引数で渡すことでも設定できます。
std::tuple<int, CMyClass, double> tupleValue = std::tuple<int, CMyClass, double>(10, instance, 15.5);
コンストラクタだけでなく、代入演算子も用意されているので、上記のような初期値としてだけでなく、定義し終わった後の Tuple 変数に対しても、上記のように一括して値を設定することができます。
Tuple の情報
Tuple が持っている要素の数は std::tuple_size を使って取得できます。
size_t count = std::tuple_size<std::tuple<int, double>>::value;
このように std::tuple_size の <> で Tuple 型を渡してそこから value を取得します。
これで、今回の場合であれば count 変数に 2 が取得されます。
ある Tuple 型の変数から要素数を取りたい場合は decltype キーワードと合わせて使用します。
size_t count = std::tuple_size<decltype(tupleValue)>::value;
このようにすれば、Tuple 型の変数 tupleValue の要素数を取得できます。
decltype の詳細については C++11 の型推論で変数定義を簡略化する を参照してください。
Tuple を構築する
Tuple を構築する関数も用意されています。
引数リストから自動で Tuple 型を構築する
与えた引数から型を判断させて自動で Tuple 型を作りたい場合は std::make_tuple 関数が使えます。
auto tupleValue = std::make_tuple(10, 15.3, &instance);
このようにすることで、たとえば instance が CMyClass 型だった場合、"std::tuple<int, double, CMyClass*>" という型の Tuple が生成されます。このとき、Tuple の各要素は、新しいメモリが確保されてそこにコピーされます。
これを auto キーワードで宣言した変数で受ければ、変数定義も簡略化できて便利です。auto の詳細については C++11 の型推論で変数定義を簡略化する を参照してください。
複数個の変数を Tuple にまとめる
複数個の変数を Tuple にまとめ上げるには std::tile 関数を使用します。
int a = 10;
double b = 0.5;
std::vector<int> c = { 1, 3 };
auto tupleValue = std::tie(a, b, c);
このようにすることで、既存の変数を簡単に Tuple にまとめることができました。
このとき Tuple に格納される変数は、参照として Tuple 内に登録されます。
同じく複数の変数を Tuple にまとめる関数として std::forward_as_tuple というものもあります。
こちらも std::tie と使い方は同じですが、std::forward_as_tuple の場合は、引数に渡された変数を右辺値参照として取るそうです。
右辺値参照の詳細については 右辺値参照とムーブコンストラクタの使い方 を参照してください。
複数の Tuple を連結する
std::tuple_cat を使うと、複数の tuple を連結して新しい tuple を作成することができます。
auto tuple1 = std::make_tuple(10, 3.3, "aa");
auto tuple2 = std::make_tuple(3.2f, "bb");
auto tuple3 = std::tuple_cat(tuple1, tuple2);
このように、2つの Tuple 変数 tuple1 と tuple2 を連結した、新しい tuple3 を生成しています。
ただ、この構文は Visual Studio 2012 では "オーバーロードされた関数 "std::tuple_cat" の複数のインスタンスが引数リストと一致します:" というコンパイルエラーになってしまうようでした。
Tuple を手軽に扱いたい場合
Tuple は型情報として、内部に持つ型を列挙する必要があるため、使い方によってはプログラムが長くなってしまいがちです。
たとえば Tuple 型をコンストラクタを使って値を初期化するような場合は、このような感じになります。
std::tuple<int, CMyClass, double> tupleValue = std::tuple<int, CMyClass, double>(10, instance, 15.5);
このように長いプログラムは読むのも書くのも億劫なので、ほかの構文と組み合わせて、少しでもそんな負担を減らしてみるのも効果的かもしれません。
型推論 auto と相性が良い
長くなりがちな Tuple 型ですけど、右辺から左辺の型をコンパイラが自動的に決定してくれる auto と相性がとても良さそうです。
auto の詳細については C++11 の型推論で変数定義を簡略化する を参照してください。
auto tupleValue = std::tuple<int, CMyClass, double>(10, instance, 15.5);
int value1 = std::get<0>(tupleValue);
CMyClass value2 = std::get<1>(tupleValue);
double value3 = std::get<2>(tupleValue);
このように、右辺で Tuple 型が持つ型情報を定義さえしてしまえば、代入先の変数 tupleValue の型は auto でそれが自動で決定されます。
その先では変数名を使って std::get<index> で値を取ることができるので、長い型定義は 1 度だけで済みます。
型推論 decltype で戻り値と同じ型を定義する
関数の戻り値で auto は使えないので、Tuple を戻り値にする関数を定義するときには、戻り値とそれを関数内で準備する変数の 2 箇所で Tuple を定義する必要があります。
ここを 1 箇所で済ませたい場合は、decltype を使って関数の戻り値の型を再利用する方法があります。
decltype の詳細については C++11 の型推論で変数定義を簡略化する を参照してください。
std::tuple<int, double> CTestClass::tupleFunction(int param)
{
auto result = decltype(CTestClass::tupleFunction(1))(param, param / 3.0);
return result;
}
このようにすると、関数内で Tuple の定義を省略することはできますが、代わりに少し複雑な decltype を記載する必要が出てきます。
関数名が長かったり引数を取る関数の場合だと読みにくくなりますし、その Tuple がどんな値を格納できるかを知るには decltype に書いてある関数の定義を見るという手間が必要なので、こうした方がプログラムが楽になるかは判りません。
typedef で別名を付ける
関数の戻り値として Tuple を使う場合や、いくつかの場所で同じ Tuple を使う必要がある場合には、typedef を使って別名を付けておくと便利です。
typedef std::tuple<int, double> SampleTuple;
このように定義すると、"typedef std::tuple<int, double>" の代わりに "SampleTuple" が使えるようになります。
これを使えば、戻り値に Tuple を取る関数を実装するときにも素直な書き方ができるようになります。
SampleTuple CTestClass::tupleFunction(int param)
{
auto result = SampleTuple(param, param / 3.0);
return result;
}
表現が変わっても Tuple であることに変わりはないので、値の取得や設定の仕方は変わりません。
SampleTuple tupleValue;
std::get<0>(tupleValue) = 10;
std::get<1>(tupleValue) = 15.5;
このように、とてもシンプルに使えるようになりました。
ただ、事前に typedef によって定義する必要があるため、それなら従来の struct 構造体と準備の手間が変わらないようにも思えます。
むしろシンプルに扱える struct 構造体の方が便利なのかもしれません。
typedef struct
{
int value1;
double value2;
}
SampleStruct;
SampleStruct structValue;
structValue.value1 = 10;
structValue.value2 = 15.5;
Tuple ではコンストラクタで値を一括設定できましたが、struct 構造体も C++11 の初期化構文で一括設定できます。
SampleStruct structValue = { 10, 15.5 };
[ もどる ]