最終更新日時:
が更新

履歴 編集

暗黙のラムダキャプチャを簡略化 [P0588R1](C++20)

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

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

概要

ここでは、ラムダ式での暗黙のキャプチャについて、以下の問題を解決する:

  1. ラムダ式内でのdecltype((x))の使用
  2. ラムダ式での構造化束縛のキャプチャ

ラムダ式でコピーキャプチャした変数を、評価されない文脈のみで使用した場合に、その変数はクロージャオブジェクトのメンバ変数になることを仮定していた。この変更ではその仮定をやめ、式の型のみを規定するようになった。

void f() {
  float x, &r = x;
  [=] {
    decltype(x) y1;        // y1はfloat型をもつ
    decltype((x)) y2 = y1; // y2はconst float&型をもつ。ラムダ式がmutableではなくxが左辺値であるため
    decltype(r) r1 = y1;   // r1はfloat&型をもつ
    decltype((r)) r2 = y2; // r2はconst float&型をもつ
  };
}

本提案では一旦、構造化束縛で導入された名前はラムダ式でキャプチャできない、と明記された。その後、「構造化束縛を拡張して通常の変数宣言のように使用できるようにする」の仕様でそれが可能となったため、この部分は打ち消された。

仕様

ラムダ式がどのローカル変数をキャプチャできるか、またそれをどこで使用できるかを規定するために、odr-usableという用語が導入された。

ローカル変数、関数のパラメータ、構造化束縛で導入された名前、*thisのように、関数のローカルな文脈に属するものをローカルエンティティ (local entity) と呼ぶ。ローカルエンティティは、その宣言から使用する地点までの間に挟まるスコープが、すべて以下のいずれかである場合に、その地点でodr-usableとなる。

  • ブロックスコープ ({}で囲まれた文のスコープ)
  • ラムダ式の関数パラメータスコープ (ラムダ式のパラメータを宣言できる範囲) であり、そのラムダ式がそのローカルエンティティを明示的にキャプチャしているかデフォルトキャプチャをもっていて、かつそのラムダ式の本体のブロックスコープも、使用する地点までの間に挟まっている

odr-usableではないローカルエンティティをodr-usedした場合、そのプログラムは不適格となる。

void f(int n) {
  [] { n = 1; };               // エラー: nをキャプチャしていないため、odr-usableではない
  [&] { n = 1; };              // OK: nをキャプチャしており、ラムダ式の本体の中で使用している

  struct A {
    void g() { n = 2; }        // エラー: ローカルクラスのスコープと、そのメンバ関数の
  };                           //         関数パラメータスコープが挟まる

  void g(int = n);             // エラー: ラムダ式ではない関数の関数パラメータスコープが挟まる

  [=](int k = n) {};           // エラー: ラムダ式の関数パラメータスコープは挟まるが、
                               //         本体のブロックスコープは挟まらない

  [&] { [n]{ return n; }; };   // OK: 内側のラムダ式は、外側のラムダ式の本体の中にある
}

ラムダ式のデフォルト引数で外側のローカル変数を使用できない理由

キャプチャが効くのはラムダ式の本体の中だけであり、パラメータの宣言はその外側にある。そのため[=](int k = n) {}のnは、キャプチャしたコピーではなく、外側のnそのものを指す。

デフォルト引数が評価されるのは、ラムダ式を書いた場所ではなく、クロージャオブジェクトを呼び出した場所である。クロージャオブジェクトはそれを作った関数の外へ持ち出せるため、仮にこれを許すと、すでに生存期間の終わったローカル変数を読むことになりうる。

auto f(int n) {
  return [=](int k = n) { return k; };  // エラー
}

int main() {
  auto g = f(3);
  g();  // 仮に適格だとすると、ここで関数f()のパラメータnを読むことになる
}

関連項目

参照

  • P0588R1 Simplifying implicit lambda capture
    • C++20で、ラムダ式での暗黙のキャプチャの規定が整理され、odr-usableが導入された
  • CWG Issue 2380. capture-default makes too many references odr-usable
    • C++20の策定中に、odr-usableの条件へ「ラムダ式の本体のブロックスコープも挟まっていること」が追加された。この条件がないと、デフォルトキャプチャを書くだけでラムダ式のパラメータのデフォルト引数から外側のローカル変数を使用できてしまっていた