ペア型とタプル型
配列は同じデータ型の値をまとめて扱うことができます。
異なるデータ型をまとめて扱うにはそのためのクラス(構造体)を自作する方法がありますが、値の保存と取り出しだけのシンプルな機能で足りる場合はペア型やタプル型を使用することができます。
ペア型
ペア型は異なる二つのデータ型を格納するクラステンプレートです。
(クラステンプレート=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
連想コンテナ型の要素
ペア型はコンテナクラスの連想配列(map
、multimap
)、ハッシュマップ(unordered_map
、unordered_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::tie
はstd::pair
やstd::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);