HOWTO · C++

C++でstd::tupleを作成・参照・出力する方法

C++で型の異なる値からstd::tupleを構築し、要素を読み書きする方法と、サイズの異なるタプルの全要素を出力する方法を解説します。

このページの内容

型の異なる決まった数のフィールド(名前、識別子、スコアなど)を1つの値にまとめるには、std::tupleを使います。このガイドでは、std::make_tupleによる構築、std::getによる要素の読み取りと更新、サイズの異なるタプルの全要素を出力する方法を説明します。タプルでは要素の位置が型の一部なので、実行時に要素数が増減するコレクションではなく、要素数が決まった小さなレコードに適しています。

std::tupleが格納する値と使いどころ

std::tupleは、固定長の値列を表すクラステンプレートです。各位置には異なる型の値を格納でき、すべての要素の型と順序はコンパイル時に決まります。たとえば、文字列、整数、浮動小数点数を1つのタプルにまとめられます。std::tupleは型であり、関数やコマンドではありません。

タプルの各位置に名前はありません。そのため、短い結果や局所的な値のまとまりには便利ですが、大きなプログラム全体でフィールド名を一貫して使いたい場合は、内容が伝わりにくくなります。名前付きフィールドによってデータを理解しやすくできるなら構造体を使い、要素数が変わるならコンテナを使いましょう。

std::make_tupleでタプルを構築する

std::make_tupleに値を渡すと対応する要素型が推論されるため、構築箇所でstd::tuple<...>のような長い型を書く必要がありません。要素の順序は引数の順序と同じです。std::tupleとstd::make_tupleを使うには<tuple>を、std::stringの値を格納するには<string>をインクルードします。以下の完全なプログラムでは、shortTupleをstd::string、int、doubleから構築します。std::make_tupleが引数から要素型を推論します。値は型に応じてタプルにコピーまたはムーブされます。これは値からタプルを構築する方法であり、実行時に要素数を変更できる列を作るものではありません。

std::getでタプルの要素を読み取り、更新する

std::get<I>(value)を使うと、位置Iの要素にアクセスできます。Iは0から始まるコンパイル時インデックスです。3要素のタプルで有効な位置は0、1、2です。以下の完全なプログラムでは、std::get<1>(profile)で整数フィールドを読み取り、originalIdに格納します。同じ式を代入の左辺に置けば、そのフィールドを更新できます。指定する位置は有効でなければならないため、実行時にだけ計算されるインデックスをstd::getに渡すことはできません。

std::tupleには角括弧によるインデックス指定はありません。実行時にインデックスを選ぶ必要がある場合は、同じ型の要素を格納するコンテナなど別の表現を使うか、既知のタプル位置に対する明示的な振り分けを記述してください。std::getで読み取ると要素の型が保たれるため、代入や関数呼び出しが実際のフィールド型に合っているかをコンパイラーが確認できます。

可変個引数テンプレートですべての要素を出力する

可変個引数テンプレートを使うと、要素数の異なるタプルを受け取れます。以下の例は畳み込み式を使うため、C++17以降が必要です。std::index_sequenceでコンパイル時の位置を生成し、畳み込み式で対応する各値をストリームに出力します。値は括弧で囲み、カンマで区切ります。各要素の型はstd::ostreamへの挿入に対応している必要があります。このプログラムはタプルを構築して更新した後、サイズの異なる別の2つのタプルを出力します。出力は次のとおりです。

この例を実行してください。

このセクションのコードと表示されるラベルや出力は英語のままです。

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

using namespace std;

template <class Tuple, size_t... I>
void printTupleImpl(const Tuple& values, index_sequence<I...>) {
  size_t printed = 0;
  ((cout << (printed++ ? ", " : "") << get<I>(values)), ...);
}

template <class... Values>
void printTuple(const tuple<Values...>& values) {
  cout << "(";
  printTupleImpl(values, index_sequence_for<Values...>{});
  cout << ")\n";
}

int main() {
  auto profile = make_tuple(string("Ada"), 7, 9.5);
  const int originalId = get<1>(profile);
  get<1>(profile) = originalId + 1;

  auto shortTuple = make_tuple(string("Ada"), 7, 9.5);
  auto longTuple = make_tuple(string("Lin"), 12, 8.25, string("C++"));
  printTuple(shortTuple);
  printTuple(longTuple);
}
(Ada, 7, 9.5)
(Lin, 12, 8.25, C++)

このセクションのコードと表示されるラベルや出力は英語のままです。

ヘルパーは、std::index_sequence_forが与える各インデックスに対してstd::get<I>式を1つずつ展開します。カンマの畳み込みによって式が順番に処理されるため、要素数ごとに別のオーバーロードを用意せずに、同じ関数でどちらのサイズのタプルも扱えます。要素がないタプルにも対応し、その場合は畳み込みで何も挿入されず、関数は括弧だけを出力します。出力する値自体はストリームに挿入できなければなりません。独自型を含むタプルでは、適切なストリーム挿入演算子が必要です。

std::tupleとstd::pairの使い分け

std::pairが格納する値はちょうど2つですが、std::tupleは2つを含む任意の固定数の値を格納できます。キーと値のように、データが自然に関連する2つのフィールドから成る場合は、その形が型から直接伝わるstd::pairが適しています。型の異なる値を3つ以上まとめる場合や、インターフェースが複数の値を意図的にまとめて返す場合は、std::tupleを選びましょう。

コードベースの広い範囲で使うデータなら、名前付き構造体のほうがどちらの型よりも意図を伝えやすいことがよくあります。一時的な値のまとまりや簡潔な戻り値には、別の型を定義せずに済むstd::tupleが便利です。どちらを使う場合もフィールドの順序を明確にし、タプルの位置にはstd::getでアクセスすると、各インデックスがどの値を表すのか読み手に伝わります。