【C++】いまさらだけどtupleを理解する

C++

タプル(tuple)とは

タプルとは、複数の型の値をひとまとめにして取り扱うためものです。元々C++には2つの値の組をひとまとめにして扱うpairがありましたが、それを一般化したようなものです。

構造体と似ていますが、構造体と違っていちいち構造を定義しなくてもよく、関数の戻り値など一時的な値のやり取りなどで手軽に扱うことができます。また可変引数テンプレートを利用できます。

一方で、構造体のように要素に名前がつかないことから、何番目に何の値が入っているかを実装者が理解しておくしかなく、コードを見て何をしているのかわかりにくいところもあります。

基本的な使い方

タプルの生成

タプルを作成するにはmake_tuple()を使います。

int a = 1;
float b = 1.2;
std::string c = "tuple";

auto tpl = std::make_tuple(a, b, c);

要素の取得

タプルの各要素にアクセスするにはget<>()を使います。

std::tuple<int, float, std::string> tpl = { 1, 1.2, "tuple" };

int a = std::get<0>(tpl);         // a = 1
float b = std::get<1>(tpl);       // b = 1.2
std::string c = std::get<2>(tpl); // c = "tuple"

get<>()の要素の指定はテンプレートなので、コンパイル時に値が決まっている必要があります。すなわち次のようなことはできません。

for(int i = 0; i < 3; i++){
    std::get<i>(tpl); // コンパイルエラー。iは変数でコンパイル時に値が決まらない
}

分割代入

タプルの各要素を個別の変数として受け取ることができます。これには、tie()を使用します。

std::tuple<int, float, std::string> tpl = { 1, 1.2, "tuple" };
int a;
float b;
std::string c;

std::tie(a, b, c) = tpl; // a = 1, b = 1.2, c = "tuple"

なお、受け取る必要のない値に対しては、ignoreを指定します。

std::tie(a, std::ignore, c) = tpl; // a = 1, c = "tuple"

C++17以降であれば、構造化束縛を使うことでよりシンプルに記述できます。

auto [a, b, c] = tpl;

このときautoは必須なので、同一スコープ内で同じ変数名を使うことができないことに注意が必要です。また、ignoreも使用できません。

タプル同士の比較

タプルには比較演算が用意されています。タプル同士を比較する場合、各要素を先頭から比較します。比較を行うためには要素数が同じで、それぞれの要素が同じ演算子で比較可能でなければなりません。

std::tuple<int, std::string> tpl1 = { 1, "abc" };
std::tuple<int, std::string> tpl2 = { 2, "abc" };
std::tuple<int, std::string> tpl3 = { 1, "def" };

std::cout << std::boolalpha;
std::cout << (tpl1 == tpl2) << std::endl;
std::cout << (tpl1 < tpl3) << std::endl;

出力結果

false
true

応用的な使い方

タプルの要素数を取得する

タプルの要素数を取得するには、tuple_size<>を使います。

std::tuple<int, float, std::string> tpl;
size_t n = std::tuple_size<decltype(tpl)>::value; // n = 3

各要素にアクセスする(C++14以降)

tupleを可変引数テンプレートとともに使用していると、タプルの各要素にアクセスしたい時が出てきます。要素へのアクセスにはget<>()を使いますが、上で述べたようにget<>()は変数を使用することができません。各要素にアクセスするには、index_sequence<>テンプレートクラスとパラメータパックの展開というテクニックを組み合わせて行います。

以下の例では、タプルの要素を標準出力に出力するPrintTupleValue()関数をタプルの各要素に適用しています。

#include <iostream>
#include <string>
#include <tuple>
#include <utility>
#include <vector>

template <typename T>
void PrintTupleValue(const T & t)
{
    std::cout << t << std::endl;
}

template <typename Tuple, size_t... I>
void PrintTupleImplement(const Tuple & tpl, std::index_sequence<I...>)
{
    using swallow = int[];
    (void)swallow{(PrintTupleValue(std::get<I>(tpl)), 0)...};
}

template <typename Tuple>
void PrintTuple(const Tuple & tpl)
{
    PrintTupleImplement(
        tpl,
        std::make_index_sequence<std::tuple_size<typename std::decay<Tuple>::type>::value>()
    );
}

int main()
{
    PrintTuple(std::make_tuple((int)0, (float)1.2, std::string("tuple")));
    return 0;
}

出力結果

0
1.2
tuple

PrintTupleImplement()関数に着目します。この関数は引数としてindex_sequence<>型を持っています。index_sequence<>は、可変引数テンプレートとしてsize_t... Iを持つ、0からI-1までのシーケンスをコンパイル時に表現するテンプレートクラスで、make_index_sequence<>テンプレートクラスを使って作成します。このindex_sequence<>が可変引数テンプレートを持つところがポイントで、パラメータパックの展開と組み合わせることができます。

パラメータパックは、関数の引数やテンプレートの引数で展開ができますが、初期化子{}の中でも展開できます。PrintTupleImplement()関数の中の(void)swallow{...}となっている部分が初期化子で、この展開を行っています。
swallowint []の別名となっており、この通りにする必要はありませんが、イディオム的に使われているようです。初期化子が使えれば何でもよく、int []である必要もありません。

また、f(args)...(式)...のように、関数や式に...をつけることでパラメータパックの各要素に共通の処理を適用することもできます。これをパラメータパックの拡張といいます。この例では、(PrintTupleValue(std:get<I>(tpl)), 0)...の形で拡張が行われています。見慣れない形ですが、これはint[]として定義しているので、整数値が必要であることから、カンマ演算子を使って, 0として0を返す形にしているだけです。実質はPrintTupleValue(std::get<I>(tpl))...で、タプルの各要素をget<>()で取り出したあとにPrintTupleValue()を呼び出しています。

ちなみに、(void)swallowvoidにキャストしている理由は、このようにしないとコンパイラが警告を出すことがあるためです。

(参考)

タプルを関数の引数にする(C++14以降)

パラメータパックは初期化子{}のほか、関数の引数でも展開できるのでした。ということは、タプルは関数の引数にすることができるはずです。

早速やってみます。上で挙げた例と同様にすべての要素を出力します。

#include <iostream>
#include <string>
#include <tuple>
#include <utility>
#include <vector>

template <typename T>
void PrintTupleValue2(const T & t)
{
    std::cout << t << std::endl;
}

template <typename T, typename... Types>
void PrintTupleValue2(const T & t, const Types &... args)
{
    std::cout << t << std::endl;
    PrintTupleValue2(args...);
}

template <typename Tuple, size_t... I>
void PrintTupleImplement2(const Tuple & tpl, std::index_sequence<I...>)
{
    PrintTupleValue2(std::get<I>(tpl)...);
}

template <typename Tuple>
void PrintTuple2(const Tuple & tpl)
{
    PrintTupleImplement2(
      tpl,
      std::make_index_sequence<std::tuple_size<typename std::decay<Tuple>::type>::value>());
}

int main()
{
    PrintTuple2(std::make_tuple((int)0, (float)1.2, std::string("tuple")));
    return 0;
}

出力結果

0
1.2
tuple

PrintTupleImplements()関数では、可変引数を持つPrintTupleValue2()関数を直接呼び出すことができます。C++17以降であれば、関数を直接呼び出すapply()という関数が使えます。以下のように使います。

template <typename... Types>
void PrintTuple3(const std::tuple<Types...> & tpl)
{
    std::apply(PrintTupleValue2<Types...>, tpl);
}

apply()の場合、可変引数テンプレートの関数を呼び出すためには、上のようにテンプレートの型を指定する必要があります。

C++

Posted by izadori