ペア型とタプル型

配列は同じデータ型の値をまとめて扱うことができます。
異なるデータ型をまとめて扱うにはそのためのクラス(構造体)を自作する方法がありますが、値の保存と取り出しだけのシンプルな機能で足りる場合はペア型タプル型を使用することができます。

ペア型

ペア型は異なる二つのデータ型を格納するクラステンプレートです。
(クラステンプレート=vectorクラスのように任意のデータ型を格納できるもの)
ペア型はstd::pair型を使用します。
これは<utility>ヘッダファイルに定義があります。


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

int main()
{
    //初期化
    std::pair<int, std::string> pair1(1, "abc");
    std::pair<int, std::string> pair2 = std::make_pair(2, "def");
    std::pair<int, std::string> pair3{ 3, "ghi" };      //C++11
    std::pair<int, std::string> pair4 = { 4, "jkl" };   //C++17

    //代入
    pair1 = pair2;
    pair2 = std::make_pair(5, "mno");
    pair3 = { 6, "pqr" };   //C++17
}

上記はすべて「int型」と「string型」をペアとするstd::pair型変数の初期化と代入です。
ペア型はstd::make_pair関数(関数テンプレート)で作成できます。
C++17以降はコピーリスト初期化(={ x, y })やリスト形式からの代入にも対応しています。
(初期化時に代入演算子=を使用せずに初期化子リストを渡す記法は一様初期化といい、C++11から可能です)

コピーリスト初期化、および初期化子リストによる代入(p = { x, y }の形式)は、first/secondメンバの両方が目的の型そのものか、暗黙的に変換可能な場合のみ可能です。


class C
{
public:
     C(std::string x) {}
};

std::pair<int, std::string> f1()
{
    //return文への初期化子リスト指定もコピーリスト初期化
    //これはOK
    return { 1, "a" };
}

std::pair<int, C> f2()
{
    //文字列リテラルからC型への直接変換はできないので
    //コンパイルエラー
    //return { 1, "a" };
	return { 1, C("a") };
}

int main()
{
	//文字列リテラルからC型へは直接変換はできず、
	//const char*→std::string型→C型という変換になる

	//OK
    std::pair<int, C> pair1(1, "abc");
    std::pair<int, C> pair2{ 2, "def" };

    //firstもしくはsecondメンバの暗黙的な変換ができない場合は
    //コピーリスト初期化はできない
    //std::pair<int, C> pair3 = { 3, "ghi" };

	//これもダメ
    //pair1 = { 3, "ghi" };
}

要素アクセス

要素へのアクセスはfirstメンバ変数とsecondメンバ変数を使用します。


std::pair<int, std::string> pair;
std::cout << pair.first << ", " << pair.second << std::endl;

pair.first = 9;
pair.second = "xyz";
std::cout << pair.first << ", " << pair.second << std::endl;
0,
9, xyz

連想コンテナ型の要素

ペア型はコンテナクラスの連想配列(mapmultimap)、ハッシュマップ(unordered_mapunordered_multimap)の要素の型として使用されています。


#include <iostream>
#include <string>
#include <map>

int main()
{
    std::map<int, std::string> map{
        {1, "abc"},
        {2, "def"},
        {3, "ghi"},
    };

    std::map<int, std::string>::iterator it = map.begin();
    std::pair el = *it;

    std::cout << el.first << ", " << el.second << std::endl;
}
1, abc

swap関数

同じデータ型を格納するstd::pair型のオブジェクト同士はswap関数で要素を入れ替えることができます。


std::pair<int, std::string> pair1(1, "abc");
std::pair<int, std::string> pair2(2, "def");

pair1.swap(pair2);
std::swap(pair1, pair2);

タプル型

ペア型は二つのデータ型のペアを持ちますが、タプル型は自由な数のデータ型を管理できます。
タプル型はstd::tuple型(クラステンプレート)を使用します。
<tuple>のインクルードが必要です。

なおstd::pair型はC++98からありますが、std::tuple型はC++11から使用できます。


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

int main()
{
    //初期化
    std::tuple<int, double, std::string>
		tuple1(1, 2.0, "abc");
    std::tuple<int, double, std::string>
		tuple2 = std::make_tuple(3, 4.0, "def");
    std::tuple<int, double, std::string>
		tuple3{ 5, 6.0, "ghi" };
    std::tuple<int, double, std::string>
		tuple4 = { 7, 8.0, "jkl" };    //C++17

    //代入
    tuple1 = tuple2;
    tuple2 = std::make_tuple(9, 10.0, "mno");
    tuple3 = { 11, 12.0, "pqr" };    //C++17
}

上記はすべて「int型」「double型」「string型」の三つの型を管理するstd::tuple型変数の初期化と代入です。
初期化と代入のルールはペア型と同じです。
std::tuple型の生成にはstd::make_tuple関数(関数テンプレート)を使用できます。

以下に説明するstd::tuple型用の関数のうち、タプル型を生成するもの以外はstd::pair型とstd::array型にも適用することができます。

要素アクセス

要素へのアクセスはstd::get関数(関数テンプレート)を使用します。
これはメンバ関数ではなくstd名前空間に属する関数です。


std::tuple<int, double, std::string> tuple;

std::cout << std::get<0>(tuple) << ", ";
std::cout << std::get<1>(tuple) << ", ";
std::cout << std::get<2>(tuple) << std::endl;

std::get<0>(tuple) = 1;
std::get<1>(tuple) = 2.0;
std::get<2>(tuple) = "abc";

std::cout << std::get<0>(tuple) << ", ";
std::cout << std::get<1>(tuple) << ", ";
std::cout << std::get<2>(tuple) << std::endl;
0, 0,
1, 2, abc

std::get関数のテンプレート実引数には、アクセスする要素のインデックスを指定します。
(非型テンプレート引数)
テンプレート引数なので、コンパイル時定数である必要があります。

関数の実引数にはstd::tupleのオブジェクトを指定します。

アクセスする要素がそのタプル型が持つ唯一のデータ型である場合は、以下のように書くことができます。


std::tuple<int, double, int> t{ 1, 2.0, 3 };

//データ型の指定
double a = std::get<double>(t);

//int型は二つあるのでコンパイルエラー
//int b = std::get<int>(t);

タプル型のサイズ

タプル型が持つ要素の数はstd::tuple_sizeで取得できます。


size_t size1 = std::tuple_size<std::tuple<int, double, std::string>>::value;
//3

std::tuple<char, int, double, std::string> t;
size_t size2 = std::tuple_size<decltype(t)>::value;
//4
size_t size3 = std::tuple_size_v<decltype(t)>; //C++17
//4

これは関数ではなくクラステンプレートです。
サイズはvalue静的メンバ変数が持っています。
C++17からvalueメンバへはstd::tuple_size_vで直接アクセスすることもできます。

テンプレート引数にはstd::tuple型をそのテンプレート引数も含めて指定するのですが、すでに生成されているオブジェクト(インスタンス)の要素数の取得はdecltypeが使用できます。

要素のデータ型取得

タプルのN番目の要素のデータ型はstd::tuple_elementクラステンプレートで取得できます。


std::tuple<int, double, std::string> t;

std::tuple_element<0, std::tuple<int, double, std::string>>::type type0;
std::tuple_element<1, decltype(t)>::type type1;
std::tuple_element_t<2, decltype(t)> type2;

std::cout << typeid(type0).name() << std::endl;
std::cout << typeid(type1).name() << std::endl;
std::cout << typeid(type2).name() << std::endl;
int
double
class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> >

※表示されるデータ型名は実装により異なります。

データ型はクラスのtype静的メンバ変数が持っています。
これはただの名前(文字列)ではなくデータ型そのものなので、変数の宣言部などに使用できます。
ここではtypeidを使用してデータ型名を取得しています。

参照を要素に持つタプル型

std::make_pairは実引数をコピーしたものからタプル型を作成します。
std::tieは実引数の参照からタプル型を作成します。


int a = 1;
double b = 2.0;
std::string c = "abc";

//コピーからタプル作成
std::tuple<int, double, std::string> t1 = std::make_tuple(a, b, c);

//参照からタプル作成
std::tuple<int&, double&, std::string&> t2 = std::tie(a, b, c);

a = 3;
b = 4.0;
c = "def";

std::cout << std::get<0>(t1) << ", ";
std::cout << std::get<1>(t1) << ", ";
std::cout << std::get<2>(t1) << std::endl;

std::cout << std::get<0>(t2) << ", ";
std::cout << std::get<1>(t2) << ", ";
std::cout << std::get<2>(t2) << std::endl;
1, 2, abc
3, 4, def

std::tiestd::pairstd::tupleの分解に利用できます。


std::tuple<int, double, std::string> t{ 1, 2.0, "abc" };

int a = 0;
double b = 0;
std::string c;

//タプルを分解
std::tie(a, b, c) = t;

std::cout << a << ", ";
std::cout << b << ", ";
std::cout << c << std::endl;

std::pair<int, std::string> p{ 3, "def" };

//ペアを分解
std::tie(a, c) = p;

std::cout << a << ", ";
std::cout << c << std::endl;
1, 2, abc
3, def

特定の要素を無視して分解

std::tieを使用してペア型やタプル型を分解するとき、必要のない要素がある場合はstd::ignoreで無視することができます。


std::tuple t{ 1, 2.0, "abc" };

int a = 0;
std::string c;

//二番目の要素は無視
std::tie(a, std::ignore, c) = t;

構造化束縛を利用して要素を分解

C++17からは、構造化束縛を利用して要素を取り出すこともできます。


std::tuple t{ 1, 2.0, "abc" };

//int型、double型、const char*型
auto [e1, e2, e3] = t;

この方法は要素を受け取る変数をあらかじめ宣言しておく必要はありません。
ただしstd::ignoreのように無視する要素を指定することはできません。

参照情報を保持してタプル型を作成

std::forward_as_tupleは引数に指定されたデータ型の参照情報を保ったままタプル型を作成します。
引数が右辺値の場合は右辺値参照となり、それ以外の場合は左辺値参照となります。
これは完全転送で使用されます。


int a = 1;
double b = 2.0;
std::string c = "abc";

std::tuple t1 = std::forward_as_tuple(a, b, c);
//std::tuple<int&, double& std::string&>型

std::tuple t2 = std::forward_as_tuple(3, 4.0, std::string("def"));
//std::tuple<int&&, double&& std::string&&>型

注意点として、一時オブジェクトをこの関数の引数に指定した場合、生存期間は延長されません。
その場合は生存期間の終了までに使用する必要があります。

タプル型の結合

複数のタプル型はstd::tuple_catで結合することができます。


std::tuple t1{ 1, 2.0 };
std::tuple t2{ std::string("abc")};
std::tuple t3{ 'a', 3.0f };

//t2はムーブしている
std::tuple<int, double, std::string, char, float>
	t = std::tuple_cat(t1, std::move(t2), t3);

std::cout << std::get<0>(t) << std::endl;
std::cout << std::get<1>(t) << std::endl;
std::cout << std::get<2>(t) << std::endl;
std::cout << std::get<3>(t) << std::endl;
std::cout << std::get<4>(t) << std::endl;
1
2
abc
a
3

タプルの要素を引数にして関数呼び出し

std::applyはタプル型の要素を引数とする関数を呼び出します。
これはC++17以降で使用可能です。


void func(int a, double b, std::string c)
{
	std::cout << a << ", ";
	std::cout << b << ", ";
	std::cout << c << std::endl;
}

int main()
{
	std::tuple t{ 1, 2.0, "abc" };
	std::apply(func, t);
}
1, 2, abc

タプルの要素からコンストラクタ呼び出し

std::make_from_tupleはタプル型の要素をクラスのコンストラクタに渡し、オブジェクトを構築します。
これはC++17以降で使用可能です。


class C
{
	int a;
	double b;
	std::string c;

public:
	C(int a, double b)
		:a(a), b(b) {}
	C(int a, double b, std::string c)
		:a(a), b(b), c(c) {}
};

int main()
{
	std::pair p{ 1, 2.0 };
	std::tuple<int, double, std::string> t{ 1, 2.0, "abc" };

	C a = std::make_from_tuple<C>(p);
	C b = std::make_from_tuple<C>(std::move(p)); //ムーブして構築
}

swap関数

同じデータ型を格納するstd::tuple型のオブジェクト同士はswap関数で要素を入れ替えることができます。


std::tuple<int, double, std::string> t1{ 1, 2.0, "abc" };
std::tuple t2{ 3, 4.0, std::string("def") };

t1.swap(t2);
std::swap(t1, t2);

std::tuple t3{ 5, 6.0, "ghi" };
//三番目の要素の型が異なるのでNG
//std::string型とconst char*型
//t1.swap(t3);