Menu

C++ の pair と tuple: 構造体なしで値をまとめる

std::pairstd::tuple が 2 つ以上の値を 1 つのオブジェクトにまとめる方法: 作り方、フィールドへのアクセス、構造化束縛、そしてそれぞれの使いどころ。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

2 つの値、1 つのオブジェクト

名前とスコア、x と y、「うまくいったか」というフラグと結果のように、2 つのものをまとめて扱いたいことがあります。そのようなまとまりごとに struct を定義することも_できます_が、使い捨てのまとまりには大げさすぎます。std::pair<utility> 由来)はちょうど 2 つの値を 1 つのオブジェクトにまとめ、std::tuple<tuple> 由来)はそれを固定数の任意個の値へと一般化します。

ここに来るまでに、すでに間接的に std::pair には出会っています。std::map の各要素は pair<const Key, Value> です。このページではそれを明示し、これらの型を作成・展開するための現代的で読みやすい方法を紹介します。

2 つのメンバは常に .first.second と呼ばれ、あなたの変数名は引き継ぎません。これは汎用型の代償です。フィールド名は位置によるものであり、説明的ではありません。

pair を作る

pair を作る一般的な方法は 3 つあり、いずれも同じオブジェクトを生み出します。

make_pair は要素の型を代わりに推論してくれます。これは C++17 より前では便利でした。今日では、波かっこ初期化とクラステンプレート引数推論(pair p{"Boris", 85};)でほとんどのケースをカバーできますが、既存のコードでは依然として make_pair をあちこちで目にするでしょう。

推論に関する落とし穴が 1 つあります。make_pair("hi", 3)pair<string, int> ではなく pair<const char*, int> を推論します。文字列リテラルは std::string ではありません。string が必要なら、明示的にそう書きましょう。make_pair(string("hi"), 3) とするか、pair の型をそのまま書き下します。そうしないと、後で予想外の比較やコピーが起きることがあります。

構造化束縛で展開する

あちこちで .first.second を読むのはすぐに読みにくくなります。名前が何も教えてくれないからです。C++17 の構造化束縛を使うと、2 つのフィールドに 1 行で本当の名前を付けられます。

これは、各要素が pair である map範囲ベースの for で回すときに真価を発揮します。it->first / it->second の代わりに、キーと値に直接名前を付けられます。

ループでは、他のコンテナ要素と同じように const auto& を使いましょう。各 pair のコピーを避けられ、読み取り専用であることを示せます。& を外すとすべての要素をコピーすることになり、大きな map では静かなパフォーマンスのバグになります。

2 つでは足りないとき: tuple

pair は 2 つの値で止まります。3 つ以上必要なときは、std::tuple が同じ発想を任意の個数に拡張したものです。波かっこ初期化または make_tuple で作り、N がコンパイル時に確定するインデックスである std::get<N> で読み出します。

get<> の中のインデックスは、コンパイル時に確定する定数でなければなりません。i が実行時の変数である get<i>(record) はコンパイルできません。tuple のフィールドは型が異なる場合があるため、要素の型は実行時ではなくコンパイル時に解決される必要があるからです。実行時のインデックスが欲しくなったら、おそらく必要なのは vector です。

構造化束縛は tuple でも使え、これが tuple を扱う読みやすい方法です。

複数の値を返す

これらの型に手を伸ばす日常的な理由は、構造体をでっち上げたり出力引数をやりくりしたりせずに、関数から複数の値を返すことです。結果を pairtuple にまとめ、呼び出し側で展開します。

3 つ以上の結果なら、同じように tuple を返します。また std::tie もあります。これは新しい変数を宣言する代わりに_既存の_変数へ展開する古い手法で、std::ignore でフィールドを無視したいときに便利です。

ただし、やめどきも大切です。同じフィールドのまとまりが複数の場所に現れる場合や、.second がスコアなのか件数なのかを毎回忘れてしまう場合は、名前付きメンバを持つ struct を定義しましょう。pairtuple は、ローカルで寿命の短いまとまりに最適です。データが 1 つの式より長く生きるようになった瞬間、名前付きフィールドに軍配が上がります。

比較とソート

便利なおまけ: pairtuple には_辞書順_で動作する比較演算子が組み込まれています。最初の要素を比較し、それが等しいときだけ次の要素に進みます。これにより、これらは完璧なソートキーになります。

フィールドの順序が重要であることに注目してください。age を先に置くと、まず年齢で、同点の場合は名前でソートされます。名前を先にした並びが欲しければ、tuple の要素の順序を入れ替えます。この既定の比較こそ、pair<priority, item> が優先度付きキューの定番イディオムである理由です。

次へ: イテレータ

これで、コンテナの周りに .first.secondit->first*it が登場するのを見てきました。pair の要素を、それが属する map と実際に結びつけているのが_イテレータ_です。次のページではイテレータをきちんと解きほぐします。begin()end() が本当に返すものは何か、++it がどのようにコンテナをたどるのか、そして C++ で最も厄介な未定義動作のいくつかを引き起こすイテレータ無効化の罠について説明します。

よくある質問

C++ における pair と tuple の違いは何ですか?

std::pair はちょうど 2 つの値を保持し、.first.second でアクセスします。std::tuple は固定数の値(0、2、3 個以上)を保持し、std::get<N>(t) でアクセスします。pair は本質的に、より分かりやすいメンバ名を持つ 2 要素の tuple です。3 つ以上のフィールドが必要なときだけ tuple を使いましょう。

C++ で tuple の要素にはどうアクセスしますか?

コンパイル時に確定するインデックスを使って std::get<N>(t) とします(例: std::get<0>(t))。C++17 以降は構造化束縛でも展開できます。auto [a, b, c] = t; とすると、各要素に名前付き変数が割り当てられます。実行時の変数で tuple をインデックス指定することはできません。std::get<i>i は定数でなければなりません。

C++ で関数から複数の値を返すにはどうすればよいですか?

std::pair または std::tuple を返し、呼び出し側で構造化束縛を使って展開します。auto [ok, value] = parse(text); のようにします。これは出力引数よりすっきりしており、使い捨ての構造体を定義せずに済みます。ただし、フィールドが 1 回の呼び出しを越えて使われる場合は、名前付きの構造体のほうが読みやすくなります。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める