C++プログラミングにおいて、2つの異なる型の値を1つのオブジェクトとして扱いたい場面は頻繁に発生します。

例えば、関数の戻り値として「成功の成否」と「計算結果」を同時に返したい場合や、座標データのように「X座標」と「Y座標」をセットで管理したい場合などが挙げられます。

こうしたニーズに応えるために、C++標準ライブラリにはstd::pairという便利なユーティリティが用意されています。

std::pairは、異なる型を持つ2つの要素を保持するシンプルな構造体です。

標準ヘッダーである<utility>に含まれており、古くから多くのライブラリや標準コンテナ(例えばstd::mapなど)で活用されてきました。

本記事では、このstd::pairの基本的な使い方から、C++17以降で導入された構造化束縛を活用した洗練された記述方法まで、実践的なコードを交えて詳しく解説します。

std::pairの基本構造と宣言

std::pairはテンプレートクラスとして定義されており、内部にfirstsecondという2つの公開メンバ変数を持っています。

これらはそれぞれ異なる型を持つことができ、非常に柔軟なデータ構造として機能します。

基本的な宣言方法

最も基本的な宣言は、テンプレート引数に型を明示する方法です。

C++
#include <iostream>
#include <utility>
#include <string>

int main() {
    // int型とstring型を持つpairを宣言
    std::pair<int, std::string> product(101, "Laptop");

    // メンバ変数へのアクセス
    std::cout << "ID: " << product.first << ", Name: " << product.second << std::endl;

    return 0;
}
実行結果
ID: 101, Name: Laptop

std::make_pairによる生成

C++11以前では、型を明示的に指定せずにstd::pairを生成するためにstd::make_pair関数がよく使われていました。

この関数は、引数から型を推論してくれるため、コードの記述量を減らすことができます。

C++
auto p1 = std::make_pair(42, "Answer");

CTAD(クラステンプレート引数推論)の活用

C++17以降では、CTAD (Class Template Argument Deduction)が導入されたため、コンストラクタの引数から型を推論させることが可能になりました。

現代的なC++においては、std::make_pairを使わなくても、直接コンストラクタで初期化するのが一般的です。

C++
// C++17以降の推奨される書き方
std::pair p2(200, 3.14); // std::pair<int, double>と推論される

これにより、コードがより簡潔で読みやすくなります。

ただし、明示的に型を指定したい場合や、特定の型への変換を期待する場合には、依然として型指定を行うこともあります。

std::pairの操作と主要な機能

std::pairは単なる値の保持だけでなく、比較演算子やコピー・ムーブといった基本的な操作をすべてサポートしています。

値の更新

メンバ変数であるfirstsecondは公開されているため、直接値を書き換えることが可能です。

C++
std::pair<std::string, int> user("Alice", 25);
user.first = "Bob";
user.second = 30;

比較演算の仕組み

std::pairは、比較演算子(==, !=, <, <=, >, >=)が定義されています。

比較は辞書順(lexicographical order)で行われます。

  1. まず、first同士を比較します。
  2. firstが等しい場合のみ、secondを比較します。

この性質は、複数の条件でソートを行いたい場合に非常に便利です。

例えば、点数の高い順、同じ点数なら名前順といったソート処理が、std::pairを要素とするstd::vectorに対して標準のstd::sortを適用するだけで実現できます。

C++
#include <iostream>
#include <vector>
#include <algorithm>
#include <utility>

int main() {
    std::vector<std::pair<int, std::string>> scores = {
        {80, "Alice"},
        {95, "Bob"},
        {80, "Charlie"}
    };

    // 昇順ソート
    std::sort(scores.begin(), scores.end());

    for (const auto& s : scores) {
        std::cout << s.first << ": " << s.second << std::endl;
    }

    return 0;
}
実行結果
80: Alice
80: Charlie
95: Bob

構造化束縛による効率的な記述

C++17における最大の進化の1つが構造化束縛(Structured Bindings)です。

これにより、firstsecondといった名前を使わずに、中身の要素を直接変数に展開して扱うことができるようになりました。

従来のアクセス方法との比較

以前のC++では、関数の戻り値としてstd::pairを受け取った際、次のようにアクセスしていました。

C++
auto result = find_user(101);
std::string name = result.first;
int age = result.second;

構造化束縛を使うと、以下のように1行で記述でき、かつ直感的な変数名を付与できます。

C++
auto [name, age] = find_user(101);
// 以降、nameとageを直接利用可能

参照による構造化束縛

構造化束縛は、値のコピーだけでなく、参照として受け取ることも可能です。

大きなオブジェクトを扱う場合や、元のpairを書き換えたい場合にはauto& [x, y]を使用します。

C++
std::pair<int, int> point(10, 20);
auto& [x, y] = point;
x = 100; // 元のpoint.firstが100に更新される

この機能は、コードの可読性を飛躍的に向上させるため、モダンなC++開発では必須のテクニックと言えます。

関数の戻り値としての活用

std::pairの代表的な用途の1つは、関数から2つの値を返すことです。

成功フラグとデータの返却

例えば、データの検索関数において、データが見つかったかどうかというフラグと、見つかった場合の値をセットで返す手法があります。

C++
#include <iostream>
#include <utility>
#include <string>
#include <map>

std::pair<bool, std::string> get_user_email(int id) {
    std::map<int, std::string> db = {{1, "test@example.com"}, {2, "hello@world.jp"}};
    
    if (db.count(id)) {
        return {true, db[id]}; // 以前はstd::make_pair(true, db[id])が必要だった
    }
    return {false, ""};
}

int main() {
    auto [success, email] = get_user_email(1);

    if (success) {
        std::cout << "Email: " << email << std::endl;
    } else {
        std::cout << "User not found." << std::endl;
    }
    
    return 0;
}

std::mapとの親和性

標準ライブラリのstd::mapstd::unordered_mapは、その要素を内部的にstd::pair<const Key, T>として保持しています。

そのため、マップのループ処理において構造化束縛は極めて強力な武器になります。

C++
#include <iostream>
#include <map>
#include <string>

int main() {
    std::map<std::string, int> inventory = {{"Apples", 50}, {"Oranges", 20}, {"Bananas", 100}};

    // キーと値を直接変数として扱う
    for (const auto& [item, count] : inventory) {
        std::cout << item << " has " << count << " units in stock." << std::endl;
    }

    return 0;
}
実行結果
Apples has 50 units in stock.
Bananas has 100 units in stock.
Oranges has 20 units in stock.

このように、ループ内でit->firstit->secondと書く必要がなくなり、コードの意図がより明確になります。

std::pairを使う際の注意点とベストプラクティス

std::pairは非常に手軽ですが、乱用するとコードの保守性を下げる恐れがあります。

意味のある名前を優先する

std::pairの最大の弱点は、要素の名前が常にfirstsecondになってしまうことです。

例えば、次のようなコードはどうでしょうか。

C++
std::pair<int, int> range(1, 100);
// range.firstは「最小値」?それとも「開始位置」?

2つの値の間に明確な主従関係や意味がない場合、あるいはそのデータ構造がプログラム内の多くの場所で使われる場合は、独自のstruct(構造体)を定義することを推奨します。

C++
struct Range {
    int min;
    int max;
};

独自の構造体を使うことで、メンバに適切な名前を付けることができ、誤用を防ぐことができます。

一方で、関数の内部的な一時処理や、限られたスコープでのみ使用される場合は、定義の手間が省けるstd::pairが非常に効率的です。

要素が3つ以上の場合はstd::tuple

要素が3つ以上になる場合は、std::pairをネストさせるのではなく、std::tupleを使用しましょう。

特徴std::pairstd::tuple独自構造体
要素数2つ固定任意任意
メンバ名first, secondget<N>(t) / 構造化束縛任意の名付けが可能
用途簡易的なペアリング3つ以上の汎用データ束意味のある永続的なデータ構造

std::pairの中にさらにstd::pairを入れる(pair<int, pair<int, int>>)といった実装は、コードの解読を困難にするため避けるべきです。

高度なトピック:std::pairの初期化とパフォーマンス

std::pairは軽量なオブジェクトですが、内部の型が複雑な場合には初期化の方法に注意が必要です。

ピースワイズ・コンストラクション

std::pairの要素となる型が、複数の引数を持つコンストラクタを必要とする場合、std::piecewise_constructを使用することで、要素をその場で構築(インプレース構築)できます。

これにより、一時オブジェクトの生成とコピーを回避し、パフォーマンスを向上させることができます。

C++
#include <iostream>
#include <utility>
#include <tuple>
#include <vector>

struct ComplexObj {
    ComplexObj(int a, double b, std::string c) {
        std::cout << "ComplexObj constructed" << std::endl;
    }
};

int main() {
    // 要素を直接構築する
    std::pair<int, ComplexObj> p(
        std::piecewise_construct,
        std::forward_as_tuple(1),
        std::forward_as_tuple(10, 3.14, "hello")
    );

    return 0;
}

この手法は主に、std::mapemplaceメソッドなどで内部的に利用されています。

通常のアプリケーションコードで直接書く機会は少ないかもしれませんが、効率を極限まで求めるライブラリ実装などでは重要なテクニックです。

まとめ

std::pairは、C++において非常に基本的でありながら、応用範囲の広い便利なツールです。

本記事のポイントを振り返ります。

  1. 基本:2つの異なる型を1つにまとめ、firstsecondでアクセスする。
  2. 生成:C++17以降はCTADにより、型指定を省略した直感的な初期化が可能。
  3. 利便性:比較演算子が辞書順で定義されているため、ソート処理との相性が良い。
  4. モダンな記述構造化束縛を用いることで、戻り値やマップの要素を劇的に読みやすく記述できる。
  5. 使い分け:複雑なデータや長期的に使用するデータには、pairではなく独自構造体の定義を検討する。

C++の標準ライブラリを使いこなす第一歩は、こうしたシンプルなユーティリティの特性を正しく理解し、最新の言語仕様に合わせて最適な書き方を選択することにあります。

構造化束縛を取り入れるだけでも、あなたの書くC++コードはより安全で、かつメンテナンス性の高いものへと進化するでしょう。