C++17で追加された構造化束縛は多値を分解して受け取るための変数宣言の文法だ。
int main()
{
int a[] = { 1,2,3 } ;
auto [b,c,d] = a ;
// b == 1
// c == 2
// d == 3
}
C++では、さまざまな方法で多値を扱うことができる。たとえば配列、クラス、tuple
, pair
だ。
int a[] = { 1,2,3 } ;
struct B
{
int a ;
double b ;
std::string c ;
} ;
B b{ 1, 2.0, "hello" } ;
std::tuple< int, double, std::string > c { 1, 2.0, "hello" } ;
std::pair< int, int > d{ 1, 2 } ;
C++の関数は配列以外の多値を返すことができる。
std::tuple< int, double, std::string > f()
{
return { 1, 2.0, "hello" } ;
}
多値を受け取るには、これまでは多値を固まりとして受け取るか、ライブラリで分解して受け取るしかなかった。
多値を固まりで受け取るには以下のように書く。
std::tuple< int, double, std::string > f()
{
return { 1, 2.0, "hello" } ;
}
int main()
{
auto result = f() ;
std::cout << std::get<0>(result) << '\n'
<< std::get<1>(result) << '\n'
<< std::get<2>(result) << std::endl ;
}
多値をライブラリで受け取るには以下のように書く。
std::tuple< int, double, std::string > f()
{
return { 1, 2.0, "hello" } ;
}
int main()
{
int a ;
double b ;
std::string c ;
std::tie( a, b, c ) = f() ;
std::cout << a << '\n'
<< b << '\n'
<< c << std::endl ;
}
構造化束縛を使うと、以下のように書ける。
std::tuple< int, double, std::string > f()
{
return { 1, 2.0, "hello" } ;
}
int main()
{
auto [a, b, c] = f() ;
std::cout << a << '\n'
<< b << '\n'
<< c << std::endl ;
}
変数の型はそれぞれ対応する多値の型になる。この場合、a
, b
, c
はそれぞれint
, double
, std::string
型になる。
tuple
だけではなく、pair
も使える。
int main()
{
std::pair<int, int> p( 1, 2 ) ;
auto [a,b] = p ;
// aはint型、値は1
// bはint型、値は2
}
構造化束縛はif
文とswitch
文、for
文でも使える。
int main()
{
int expr[] = {1,2,3} ;
if ( auto[a,b,c] = expr ; a )
{ }
switch( auto[a,b,c] = expr ; a )
{ }
for ( auto[a,b,c] = expr ; false ; )
{ }
}
構造化束縛はrange-based for
文にも使える。
int main()
{
std::map< std::string, std::string > translation_table
{
{"dog", "犬"},
{"cat", "猫"},
{"answer", "42"}
} ;
for ( auto [key, value] : translation_table )
{
std::cout<<
"key="<< key <<
", value=" << value << '\n' ;
}
}
これは、map
の要素型std::pair<const std::string, std::string>
を構造化束縛[key, value]
で受けている。
構造化束縛は配列にも使える。
int main()
{
int values[] = {1,2,3} ;
auto [a,b,c] = values ;
}
構造化束縛はクラスにも使える。
struct Values
{
int a ;
double d ;
std::string c ;
} ;
int main()
{
Values values{ 1, 2.0, "hello" } ;
auto [a,b,c] = values ;
}
構造化束縛でクラスを使う場合は、非static
データメンバーはすべて1つのクラスのpublic
なメンバーでなければならない。
構造化束縛はconstexpr
にはできない。
int main()
{
constexpr int expr[] = { 1,2 } ;
// エラー
constexpr auto [a,b] = expr ;
}
構造化束縛は、変数の宣言のうち、**構造化束縛宣言(structured binding declaration)**に分類される文法で記述する。構造化束縛宣言となる宣言は、単純宣言(simple-declaration)とfor-range
宣言(for-range-declaration)のうち、[
識別子リスト]
があるものだ。
単純宣言:
属性 auto CV修飾子(省略可) リファレンス修飾子(省略可)
[ 識別子リスト ] 初期化子 ;
for-range宣言:
属性 auto CV修飾子(省略可) リファレンス修飾子(省略可)
[ 識別子リスト ] ;
識別子リスト:
コンマで区切られた識別子
初期化子:
= 式
{ 式 }
( 式 )
以下は単純宣言のコード例だ。
int main()
{
int e1[] = {1,2,3} ;
struct { int a,b,c ; } e2{1,2,3} ;
auto e3 = std::make_tuple(1,2,3) ;
// "= 式"の例
auto [a,b,c] = e1 ;
auto [d,e,f] = e2 ;
auto [g,h,i] = e3 ;
// "{式}", "(式)"の例
auto [j,k,l]{e1} ;
auto [m,n,o](e1) ;
// CV修飾子とリファレンス修飾子を使う例
auto const & [p,q,r] = e1 ;
}
以下はfor-range
宣言の例だ。
int main()
{
std::pair<int, int> pairs[] = { {1,2}, {3,4}, {5,6} } ;
for ( auto [a, b] : pairs )
{
std::cout << a << ", " << b << '\n' ;
}
}
構造化束縛の構造化束縛宣言は以下のように解釈される。
構造化束縛宣言によって宣言される変数の数は、初期化子の多値の数と一致していなければならない。
int main()
{
// 2個の値を持つ
int expr[] = {1,2} ;
// エラー、変数が少なすぎる
auto[a] = expr ;
// エラー、変数が多すぎる
auto[b,c,d] = expr ;
}
構造化束縛宣言で宣言されるそれぞれの変数名について、記述されたとおりの属性、CV修飾子、リファレンス修飾子の変数が宣言される。
初期化子が配列の場合、それぞれの変数はそれぞれの配列の要素で初期化される。
リファレンス修飾子がない場合、それぞれの変数はコピー初期化される。
int main()
{
int expr[3] = {1,2,3} ;
auto [a,b,c] = expr ;
}
これは、以下と同じ意味になる。
int main()
{
int expr[3] = {1,2,3} ;
int a = expr[0] ;
int b = expr[1] ;
int c = expr[2] ;
}
リファレンス修飾子がある場合、変数はリファレンスとなる。
int main()
{
int expr[3] = {1,2,3} ;
auto & [a,b,c] = expr ;
auto && [d,e,f] = expr ;
}
これは、以下と同じ意味になる。
int main()
{
int expr[3] = {1,2,3} ;
int & a = expr[0] ;
int & b = expr[1] ;
int & c = expr[2] ;
int && d = expr[0] ;
int && e = expr[1] ;
int && f = expr[2] ;
}
もし、変数の型が配列の場合、配列の要素はそれぞれ対応する配列の要素で初期化される。
int main()
{
int expr[][2] = {{1,2},{1,2}} ;
auto [a,b] = expr ;
}
これは、以下と同じ意味になる。
int main()
{
int expr[][2] = {{1,2},{1,2}} ;
int a[2] = { expr[0][0], expr[0][1] } ;
int b[2] = { expr[1][0], expr[1][1] } ;
}
構造化束縛宣言の初期化子の型E
が配列ではない場合で、std::tuple_size<E>
が完全形の名前である場合、
構造化束縛宣言の初期化子の型をE
, その値をe
とする。構造化束縛宣言で宣言される1つ目の変数を0, 2つ目の変数を1, ..., とインクリメントされていくインデックスをi
とする。
std::tuple_size<E>::value
は整数のコンパイル時定数式で、その値は初期化子の値の数でなければならない。
int main()
{
// std::tuple< int, int, int >
auto e = std::make_tuple( 1, 2, 3 ) ;
auto [a,b,c] = e ;
// std::tuple_size<decltype(e)>::sizeは3
}
それぞれの値を取得するために、非修飾名get
が型E
のクラススコープから探される。get
が見つかった場合、それぞれの変数の初期化子はe.get<i>()
となる。
auto [a,b,c] = e ;
という構造化束縛宣言は、以下の意味になる。
type a = e.get<0>() ;
type b = e.get<1>() ;
type c = e.get<2>() ;
そのようなget
の宣言が見つからない場合、初期化子はget<i>(e)
となる。この場合、get
は連想名前空間から探される。通常の非修飾名前検索は行われない。
// ただし通常の非修飾名前検索は行われない
type a = get<0>(e) ;
type b = get<1>(e) ;
type c = get<2>(e) ;
構造化束縛宣言で宣言される変数の型は以下のように決定される。
変数の型type
は"std::tuple_element<i, E>::type
"となる。
std::tuple_element<0, E>::type a = get<0>(e) ;
std::tuple_element<1, E>::type b = get<1>(e) ;
std::tuple_element<2, E>::type c = get<2>(e) ;
以下のコードは、
int main()
{
auto e = std::make_tuple( 1, 2, 3 ) ;
auto [a,b,c] = e ;
}
以下とほぼ同等の意味になる。
int main()
{
auto e = std::make_tuple( 1, 2, 3 ) ;
using E = decltype(e) ;
std::tuple_element<0, E>::type a = std::get<0>(e) ;
std::tuple_element<1, E>::type b = std::get<1>(e) ;
std::tuple_element<2, E>::type c = std::get<2>(e) ;
}
以下のコードは、
int main()
{
auto e = std::make_tuple( 1, 2, 3 ) ;
auto && [a,b,c] = std::move(e) ;
}
以下のような意味になる。
int main()
{
auto e = std::make_tuple( 1, 2, 3 ) ;
using E = decltype(e) ;
std::tuple_element<0, E>::type && a = std::get<0>(std::move(e)) ;
std::tuple_element<1, E>::type && b = std::get<1>(std::move(e)) ;
std::tuple_element<2, E>::type && c = std::get<2>(std::move(e)) ;
}
上記以外の場合、構造化束縛宣言の初期化子の型E
はクラス型で、すべての非static
データメンバーはpublic
の直接のメンバーであるか、あるいは単一の曖昧ではないpublic
基本クラスのメンバーである必要がある。E
に匿名union
メンバーがあってはならない。
以下は型E
として適切なクラスの例である。
struct A
{
int a, b, c ;
} ;
struct B : A { } ;
以下は型E
として不適切なクラスの例である。
// public以外の非staticデータメンバーがある
struct A
{
public :
int a ;
private :
int b ;
} ;
struct B
{
int a ;
} ;
// クラスにも基本クラスにも非staticデータメンバーがある
struct C : B
{
int b ;
} ;
// 匿名unionメンバーがある
struct D
{
union
{
int i ;
double d ;
}
} ;
型E
の非static
データメンバーは宣言された順番で多値として認識される。
以下のコードは、
int main()
{
struct { int x, y, z ; } e{1,2,3} ;
auto [a,b,c] = e ;
}
以下のコードと意味的に等しい。
int main()
{
struct { int x, y, z ; } e{1,2,3} ;
int a = e.x ;
int b = e.y ;
int c = e.z ;
}
構造化束縛はビットフィールドに対応している。
struct S
{
int x : 2 ;
int y : 4 ;
} ;
int main()
{
S e{1,3} ;
auto [a,b] = e ;
}
機能テストマクロは__cpp_structured_bindings
, 値は201606。