C++初心者なので, 誤りなどがあるかもしれません><;

構造化束縛

C++17には構造化束縛(Structured bindings)という機能が存在します1.
これは, 配列2std::tuple3の各要素, クラス4のpublicな5各メンバ変数を分解して受け取る機能です. 下にサンプルコードを示します.

#include <tuple>

struct Hoge {
  int i;
  double d;
  unsigned int u;
};

int main() {
  int a[] = {1, 3, 5};
  auto [a1, a2, a3] = a;
  // 配列の各要素をを構造化束縛宣言で取り出す.
  // a1 == 1, a2 == 3, a3 == 5.

  auto t = std::make_tuple(1, "po", 5.0);
  auto [t1, t2, t3] = t;
  // std::tupleの各要素を構造化束縛宣言で取り出す.
  // t1 == 1, t2 == "po", t3 == 5.0.

  Hoge h = {1, 5.0, 100};
  auto [h1, h2, h3] = h;
  // 構造体の各publicメンバ変数を構造化束縛宣言で取り出す.
  // h1 == 1, h2 == 5.0, h3 == 100.
}

最初はint型の配列の各要素を, 2番目はstd::tupleの各要素を, 3番目はHoge構造体のpublicな各メンバ変数を分解して受け取っています.
構造化束縛ではautoキーワードを使い, 分解されたそれぞれの変数の型は明示的には宣言しません. そして, 分解して定義されたそれぞれの変数の型が私の直感とは少し異なっていたというお話です.

型推論

C++11からはautoキーワードを使い, 型推論を用いた変数宣言が出来るようになりました.
ここでは本筋に関係あるところだけ, 厳密には間違っているような雑な説明だけします. 詳細な話はEffective Modern C++などを参考にすると良いかと思います.

autoによる型推論

ざっくりというとauto 変数名 = 式6という変数宣言の場合, この変数の型は式の型から参照とconstvolatileが取り払われたものになります.
実際に型推論させた変数の型をBoost.Typeindexを用いて型情報を調べてみましょう.

#include <iostream>
#include <boost/type_index.hpp>

template<typename T1, typename T2>
void output_types() {
  //  T1の型とT2の型を出力する.
  //      T1の型, T2の型
  //  のように出力される.
  std::cout << boost::typeindex::type_id_with_cvr<T1>().pretty_name()
	    << ", "
	    << boost::typeindex::type_id_with_cvr<T2>().pretty_name()
	    << std::endl;
}

int main() {
  int a1 = 0;
  auto b1 = a1;
  output_types<decltype(a1), decltype(b1)>();
  // a1はint型, b1もint型.

  const double a2 = 0;
  auto b2 = a2;
  output_types<decltype(a2), decltype(b2)>();
  // a2はconst double型, b2はdouble型.

  int &a3 = a1;
  auto b3 = a3;
  output_types<decltype(a3), decltype(b3)>();
  // a3はint&型, b3はint型.

  const double &a4 = a2;
  auto b4 = a4;
  output_types<decltype(a4), decltype(b4)>();
  // a4はconst double&型, b4はdouble型.
}

出力

int, int
double const, double
int&, int
double const&, double

出力を見ると, 型推論を用いて宣言したb1~b4の型はたしかに参照とconstが取れているのがわかるかと思います.
きちんとした話をすると, 推論規則は3種類に場合分けできてユニバーサル参照のときは云々……となると思うのですが, 厳密な話は先に上げた本なり規格書なりを参考にしてください.

decltype(auto)による型推論

decltype(auto) 変数名 = 式という形で型推論を行うこともできます.
この場合, autoキーワードによる推論ではあった参照やconstが取り払われる振る舞いがなくなります.

#include <iostream>
#include <boost/type_index.hpp>

template<typename T1, typename T2>
void output_types() {
  //  T1の型とT2の型を出力する.
  //      T1の型, T2の型
  //  のように出力される.
  std::cout << boost::typeindex::type_id_with_cvr<T1>().pretty_name()
	    << ", "
	    << boost::typeindex::type_id_with_cvr<T2>().pretty_name()
	    << std::endl;
}

int main() {
  double a5 = 0.0;
  decltype(auto) b5 = a5;
  output_types<decltype(a5), decltype(b5)>();
  // a5はdouble型, b5もdouble型.

  const int a6 = 0;
  decltype(auto) b6 = a6;
  output_types<decltype(a6), decltype(b6)>();
  // a6はconst int型, b6もconst int型.

  const double &a7 = a5;
  decltype(auto) b7 = a7;
  output_types<decltype(a7), decltype(b7)>();
  // a7はconst double&型, b7もconst double&型.
}

出力

double, double
int const, int const
double const&, double const&

decltype(auto)で型推論させた変数の型は, 初期化に用いた変数の型と同じ型になっているのがわかります.
この形式の型推論も丸括弧をつけると振る舞いが変わったりするそうなのですが, その辺の話はよくわかりません><;

構造化束縛によって決まる型

クラスのpublicな各メンバ変数を構造化束縛で分解して受け取った時, どのような型になるのでしょうか.
構造化束縛はautoキーワードを使って宣言するので直感的には, 各メンバ変数をautoで型推論した型になりそうです.
しかし, C++17の規格書相当のN4659の11.5.4項を見てみると, 分解されたそれぞれの変数の型は元のクラスで定義された型になる7と読み取れます.
つまり, autoを用いた型推論の型になるわけではないということです. 具体的にどのような差があるか見ていきましょう.

#include <iostream>
#include <boost/type_index.hpp>

template<typename T1, typename T2>
void output_types() {
  //  T1の型とT2の型を出力する.
  //      T1の型, T2の型
  //  のように出力される.
  std::cout << boost::typeindex::type_id_with_cvr<T1>().pretty_name()
	    << ", "
	    << boost::typeindex::type_id_with_cvr<T2>().pretty_name()
	    << std::endl;
}

struct Hoge {
  const int a;
  double &b;
  // const int型, double&型のメンバ変数.
  Hoge(const int a, double &b): a(a), b(b){
 }
};

int main() {
  double d = 3.14;
  Hoge h(1, d);

  auto a1 = h.a;
  auto a2 = h.b;
  // 各要素を直接型推論して受け取る場合.
  auto [b1, b2] = h;
  // 構造化束縛で受け取る場合.

  output_types<decltype(a1), decltype(b1)>();
  // a1はint型, b1はconst int型.
  output_types<decltype(a2), decltype(b2)>();
  // a2はdouble型, b2はdouble&型.

  decltype(auto) c1 = h.a;
  decltype(auto) c2 = h.b;
  // 各要素をdecltype(auto)で直接型推論して受け取る場合.

  output_types<decltype(c1), decltype(b1)>();
  // c1はconst int型, b1もconst int型.
  output_types<decltype(c2), decltype(b2)>();
  // c2はdouble&型, b2もdouble&型.
}

出力

int, int const
double, double&
int const, int const
double&, double&

Hoge構造体は, const int型のメンバ変数adouble&型のメンバ変数bを持っています.
直接メンバ変数にアクセスして型推論を用いて変数の初期化を行うと, constや参照が取り外され, それぞれの変数の型はintdoubleになります.
一方で構造化束縛で変数を宣言した場合, それぞれの変数の型はメンバ変数の型と同じconst int型とdouble&型になります. この性質はdecltype(auto)を用いて型推論を行い変数宣言した場合と似ています8.

構造化束縛のその他の性質

std::tuple

std::tupleを構造化束縛により分解して受け取った場合の型の決定規則は上述したものとは違います.
分解しようとする型Eに対し, i番目に受け取った変数の型はstd::tuple_element<i, E>::typeで決まるようです. この場合も, autoの型推論ではあった参照やconst外しの操作が行われないことがわかります.

構造化束縛の構文

構造化束縛の構文は以下のようになります(optはオプション).
attribute-specifier-seq(opt) decl-specifier-seq ref-qualifier(opt) [identifier-list] initializer;
正直これだとなにがなんだかわからないと思いますが, この構文の中の decl-specifier-seq が今までautoキーワードを用いてた部分になります.9
decl-specifier-seq は型指定子(type-specifier)を含むのですが, 構造化束縛の際には型指定子としてautoしか用いてはいけないようです. なので

decltype(auto) [a1, a2, a3] = std::make_tuple(1, 1.3, "po");

は構文エラーになります. decltype(auto)に似た規則で型が定まるのに, decltype(auto)と書いてはいけないんですね….

まとめ

構造化束縛はC++17の便利な機能.
しかし, autoキーワードを使って宣言するのに, それぞれの型はautoキーワードを使った型推論とは異なった規則で定まる. とくに参照やconst, volatileがある場合は注意しないといけない.

参考文献


  1. 構造化束縛宣言(Structured bindings declarations)のほうが正しいのかもしれませんが, 日本語だとほとんど構造化束縛と呼ばれているので本記事でもこのように記載します. ↩︎

  2. std::arrayではなくC言語スタイルの配列の方です. ↩︎

  3. std::tupleに似た機能を持つクラスも対象です. 雑に言うとstd::tuple_size<T>が使えたり, get<i>メンバ関数かstd::get<i>で各要素が取り出せるものです. std::pairstd::shared_ptr(C++20以降)などが当てはまります. ↩︎

  4. 無名共用体(anonymous union)メンバがなく, 全ての非staticなメンバ変数がpublicである(C++17の場合)もしくは…みたいな条件があります. ↩︎

  5. C++20からは, publicなメンバ変数である必要はなくなるようです(N4761 9.5.5項). ↩︎

  6. ここで考えているのはちゃんと書くと, auto identifer = assignment-expression という形式ものです(多分). ↩︎

  7. 正確には, 構造化束縛の際にconstvolatileや参照修飾子を用いて宣言しているならこれらも反映した型になります. ↩︎

  8. どんな状況でも各要素をdecltype(auto)で型推論させて受け取った場合と同じになる, と言い切っていいのかどうかは(知識不足なので)よくわかりません. ↩︎

  9. const autoauto &なども decl-specifier-seq に相当します. ↩︎