最終更新日時:
が更新

履歴 編集

ラムダ式のキャプチャが書かれた順に初期化されることを保証 [P3847R1](C++29)

このページはC++29に採用された言語機能の変更を解説しています。

のちのC++規格でさらに変更される場合があるため関連項目を参照してください。

概要

C++26までの規格では、ラムダ式のキャプチャに対応するクロージャ型のメンバ変数の宣言順は未規定であり、キャプチャの初期化もその未規定の順序で行われていた。そのため、次のようなコードは安全ではなかった。

auto x = std::make_unique<int>();
[value = *x,
 lifetime = std::move(x)] {};

valueより先にlifetimeが初期化される実装では、*xの読み取りの前にxからのムーブが行われてしまう。破棄の順序も同様に未規定だった。

C++29では、明示的なキャプチャ(x&xのような通常のキャプチャと、初期化キャプチャ)に対応するメンバ変数が、キャプチャを書いた順序と一致する順で宣言されること、および初期化がその順序で行われることが保証される。破棄は初期化の逆順で行われる。これによって、前述のコードや、あるキャプチャの初期化子がほかのキャプチャ対象の状態に依存するコードを、安全に書けるようになる。

この変更は欠陥報告 (DR) として扱われる。主要な処理系はいずれも、もともとキャプチャを書いた順序でメンバ変数を宣言・初期化していたため、以前のバージョンに対しても遡及して適用される。

仕様

  • クロージャ型の非静的メンバ変数は未規定の順序で宣言されるが、明示的なキャプチャに対して導入されるメンバ変数は、そのキャプチャの順序と一致する順序で宣言される
    • 暗黙的なキャプチャ(キャプチャデフォルト[=][&]による捕捉)に対応するメンバ変数の宣言順は、引き続き未規定である
  • キャプチャの初期化は、ラムダ式の評価時に、メンバ変数の宣言順序(部分的に未規定)および明示的なキャプチャの順序と一致する順序で行われる。これによって、破棄が構築の逆順で行われることが保証される
  • あわせて、参照の初期化キャプチャ([&r = x]のような、対応するメンバ変数を持たない場合があるキャプチャ)の初期化が規定されていなかった欠陥も修正された

#include <iostream>
#include <utility>
#include <vector>

int main()
{
  std::vector<int> v = {1, 2, 3};

  // 明示的なキャプチャは、書かれた順に初期化されることが保証される。
  // sizeの初期化はownへのムーブより前に行われるため、元の要素数が得られる
  auto f = [size = v.size(), own = std::move(v)] {
    std::cout << size << std::endl;
  };
  f();
}

出力

3

この機能が必要になった背景・経緯

クロージャ型のメンバ変数の順序を未規定とする規定は、初期化キャプチャの導入より前に書かれたものであり、主に暗黙的なキャプチャの捕捉順を実装が追跡しなくてよいようにすることを意図していたと考えられる。しかし、この実装の自由がもたらす利点(パディングの最小化など)はクロージャではほとんど問題にならず、実際にこの自由を利用する主要な実装も存在しなかった。むしろ初期化キャプチャは、順序が保証される初期化子付きの宣言と見た目がよく似ているため、順序が未規定であることは利用者にとって特に意外な落とし穴となっていた。

順序を強制する回避策として全キャプチャ対象をstd::pairなどにまとめる方法はあったが使い勝手が悪く、Itanium C++ ABIもクロージャ型のレイアウトとしてキャプチャ順を規定する意向であったことから、既存の実装の動作をそのまま標準化する形で順序が保証された。

関連項目

参照