C++14 のラムダ式 完全解説 中編

この記事では,前編に引き続き, C++14 のラムダ式について説明していく.
前編では,ラムダ式に対する大雑把な説明と,ラムダ式の持つ型推論機能を紹介した.
この記事では,ラムダ式の最も重要な機能の一つである,変数のキャプチャについて説明したい.
なお,初めて この記事を読む方は,先に前編を読むことをお勧する.


目次(再掲):

  1. ラムダ式の基礎 (前編で解説)
  2. ラムダ式型推論前編で解説)
  3. 変数のキャプチャ(この記事で解説)
  4. ラムダ式の活用法(執筆中)
  5. マニア向けの補足(執筆中)

変数のキャプチャ

ラムダ式により生成されるクラスの operator() の関数本体は, this ポインタやメンバ変数・関数の扱いを除き,ラムダ式の本体と同じコードになる.

// 例えば,
auto f = [] (int x, int y) { return x + y; }
// 上のコードは,
struct f_ {
  auto operator()(int x, int y) const {
    return x + y;
  }
};
// と同じになる.
// this ポインタの扱いに関しては,あとで説明する.


つまり,ラムダ式の本体の記述には,基本的には関数内クラスで使えるものと同じ物,
例えばグローバル変数や static 変数, constexpr なローカル変数などは自由に使うことができる.

// グローバル変数
std::string str = "hoge";

int main()
{
  // ローカルに定義された定数
  constexpr int n = 10;
  // グローバル変数やローカルで定義された定数は,ラムダ式中で使用できる
  auto f = [] {
    for ( int i = 0; i < n; ++i ) {
      std::cout << str << std::endl;
    }
  };
  f();  // ok
}


一方で,関数内クラスでローカル変数が使えないのと同じように,
ラムダ式の内部では, constexpr でないローカル変数(や関数の引数)は そのままでは 使えない.

void f(int x)
{
  auto g = [] (int y) { return x + y; };  // NG; 引数 x は そのままでは使えない
  auto h = [] { return g(1) * 2; };  // NG; ローカル変数 g は そのままでは使えない
}

それでは不便なので,ラムダ式にはローカル変数のキャプチャという機能が用意されている.


ラムダ式中でのローカル変数へのアクセスは, [] の部分を [&] にすることで可能になる.

void f(int x)
{
  auto g = [&] (int y) { return x + y; };  // OK
  auto h = [&] { return g(1) * 2; };  // OK
  std::cout << h() << std::endl;  // (x + 1) * 2
}

この機能を,ローカル変数の 参照キャプチャ と呼ぶ.
参照キャプチャを使った場合,ラムダ式中で使われている変数は,ラムダ式の外の変数と全く同じものになる.
つまり,ラムダ式の外で変数を変更すると,ラムダ式の中の変数も変更される.

void f(int x)
{
  auto g = [&] (int y) { return x + y; };  // OK
  auto h = [&] { return g(1) * 2; };  // OK
  std::cout << h() << std::endl;  // (x + 1) * 2
  x = 1;  // x を変更すると h() の結果も変わる
  std::cout << h() << std::endl;  // 元々の x の値に関わらず (1+1) * 2 = 4 になる
}

逆に,ラムダ式の中で変数を変更すると,ラムダ式の外の変数も変更される.

void f()
{
  int x = 0;
  auto g = [&] (int y) { x += y; };  // OK, ラムダ式中で参照キャプチャされた変数を変更する
  g(3);
  std::cout << x << std::endl;  // 3
  g(10);
  std::cout << x << std::endl;  // 13
}

これは, C++ 以外の多くの言語におけるラムダ式(もしくは関数内関数)の挙動に近い.

-- 例として Lua を挙げる
function f(x)
  local function g(y)
    return x + y
  end
  x = 10
  print( g(3) )  -- x の元の値に関わらず 13 になる
end

そのため,他の言語のラムダ式を知っているなら,すんなり理解できるだろう.


このように便利な参照キャプチャであるが,何故 この挙動がデフォルトとなっていないかと言うと,
C++ には言語組み込みの GC が無いため,状況によっては寿命問題が起きる可能性があるからだ.

// ラムダ式を返す関数(注: 安全ではない)
auto f(int x) {
  return [&] (int y) {
    return x + y;
  };
}

int main()
{
  auto g = f(1);
  std::cout << g(2) << std::endl;  // undefined behavior; g 内部で使われている f の引数 x は寿命が切れている
}

この問題は,関数(ラムダ式を含む)の戻り値として使った場合の他,
std::function 等の type erasure と組み合わせた時などに発生しうる.*1

int main()
{
  using function_t = std::function<int(int)>;  // typedef よりこっちの方が好き
  std::vector<function_t> vec;
  {
    int x = 10;
    vec.push_back(
      [&] (int y) {
        return x + y;
      }
    );
  }
  for ( auto& f : vec ) {
    std::cout << f(2) << std::endl;  // undefined behavior; f で使われている x は寿命が切れている
  }
}

気をつけるべきは「関数の戻り値」や「再代入をはじめとするオブジェクトへの状態変更*2」で,
これらが関わる場合には,参照キャプチャは危険であり,使うことはできない.


そのような場合に用いるのが,変数の コピーキャプチャ である.
これは,ローカル変数をコピーした値をラムダ式内部で保持する機能であり(そのため「値キャプチャ」とも呼ばれる),
独立した変数となるため寿命問題が起きにくい分,参照キャプチャより安全である.*3


使い方は,

auto f(int x, int y) {
  return [x, y] (int z) {  // x と y を「キャプチャ」する
    return x + y + z;
  };
}

int main()
{
  auto g = f(1, 2);
  std::cout << g(3) << std::endl;  // OK; 1 + 2 + 3 = 6 を表示する

  using function_t = std::function<int(int)>;  // typedef よりこっちの方が好き
  std::vector<function_t> vec;
  {
    int x = 10;
    vec.push_back(
      [x] (int y) {  // x を「キャプチャ」する
        return x + y;
      }
    );
  }
  for ( auto& h : vec ) {
    std::cout << h(2) << std::endl;  // OK
  }
}

のように,使いたい変数の名前を [] の中にカンマ区切りで書けばよい.


ただし,ラムダ式中で使いたい変数がメンバ変数の場合には,そのままでは上手くいかない.

struct Hoge
{
  int x;
  
  auto foo() const {
    return [x] (int y) { return x + y; };  // NG
  }
};

この場合, x をローカルで定義しなおすか,後述の 汎用ラムダキャプチャ構文 を使う必要がある.

struct Hoge
{
  int x;
  
  auto foo() const {
    auto& x = this->x;
    return [x] (int y) { return x + y; };  // OK
  }

  // もしくは
  /*
  auto foo() const {
    return [x = x] (int y) { return x + y; };  // OK; 後で説明する
  }
  */
};

いずれにせよ,コンパイルエラーになったら対処する,で問題ない.


また, [=] と書くことで,変数名のリストを省略することもできる.

auto f(int x, int y) {
  return [=] (int z) {  // = を使うことで,コピーキャプチャする変数を省略できる
    return x + y + z;
  };
}

int main()
{
  auto g = f(1, 2);
  std::cout << g(3) << std::endl;  // OK
}

が,コピーキャプチャは後述の理由で効率が良くない上に,
[=] を使った場合はメンバ変数が暗黙で参照キャプチャされてしまう((厳密に言えば this を値キャプチャしてるのだが,実質的には参照キャプチャである. *this が破棄された時点で dangling reference となる.))ため,
筆者は具体的に名前を羅列することを 強く推奨している.


コピーキャプチャによってキャプチャされた変数は,ラムダ式によって生成されたクラスのメンバ変数となる.
つまり,

auto f(int x, int y) {
  return [x, y] (int z) {
    return x + y + z;
  };
}

これは,

auto f(int x, int y) {
  struct Internal {
    int x; int y;
    auto operator()(int z) const {
      return x + y + z;  // ここで参照される x や y はクラスのメンバ変数
    }
  };
  return Internal{x, y};
}

これと(細かい挙動の差はあるものの)ほとんど同じになる.


メンバ変数は元の変数とは独立しているので,元の変数を変更しても,キャプチャされた値は変化しない.

int main()
{
  int x = 0;
  auto f = [x] (int y) { return x + y; };
  x = 23;
  std::cout << f(10) << std::endl;  // 10 と出力される. 33 にはならない
}

ただし,ポインタ等を使った場合には,(当たり前だが)この限りではない.

int main()
{
  int x = 0;
  int* p = &x;
  auto f = [p] (int y) { return *p + y; };
  x = 23;
  std::cout << f(10) << std::endl;  // 33
}

また,ラムダ式の生成するクラスの operator() はデフォルトで const 修飾されるため,
キャプチャされた値をラムダ式の内部で変更することは,デフォルトでは不可能である.

int sum = 0;
auto acc = [sum] (int x) {
  sum += x;  // NG
  return sum;
};

どうしても変更したい場合は,() の後に mutable キーワードを付与する.

int sum = 0;
auto acc = [sum] (int x) mutable {
  sum += x;  // OK
  return sum;
};

とはいえ,上記のような例は スマートポインタを使うなり単に参照キャプチャするなりすれば良い話であり,
標準アルゴリズムと組み合わせた時など,意外と間違いが起きやすいため,
実際のコードで mutable 修飾を使うケースは あまり存在しないし,使わないようにした方が良いだろう.


さて,コピーキャプチャされた変数はメンバ変数となるので,大量のオブジェクトをラムダ式にコピーキャプチャした場合,
または大きなサイズのオブジェクトをコピーキャプチャした場合には,生成されるラムダ式のサイズは大きくなる.

auto f() {
  std::array<int, 100000000> a;
  for( int i = 0; i < a.size(); ++i ) {
    a[i] = std::rand();
  }
  return [a] (int i) { return a[i]; };
}
int main()
{
  auto g = f();
  std::cout << sizeof(g) << std::endl;  // 100000000 * sizeof(int) くらい
}

上記のようなケースは, C++11 では, std::shared_ptr を使うことで解決できる.

auto f() {
  using array_t = std::array<int, 100000000>;
  auto p = std::make_shared<array_t>();
  auto& a = *p;
  for( int i = 0; i < a.size(); ++i ) {
    a[i] = std::rand();
  }
  return [p] (int i) { return (*p)[i]; };
}
int main()
{
  auto g = f();
  std::cout << sizeof(g) << std::endl;  // sizeof(std::shared_ptr<int>) と同じくらい
}

変数の数が多い場合には,関数内クラスか std::tuple と組み合わせた上で std::shared_ptr を使うと良い.


上記のコードは std::array ではなく std::vector を使えばよいと思うかもしれないが,
その場合はコピーキャプチャの際に変数のコピーが発生するため,思うほど速くはならない.((ただし,スタックを食いつぶさないようにする,という点においては, std::vector の方が良い.))
また, std::shared_ptr ではなく std::unique_ptr を使えばよいと思うかもしれないが,
ラムダ式のコピーキャプチャはコピーコンストラクタを要求するため,やはり上手くいかない.


そこで登場するのが, C++14 で追加された 汎用ラムダキャプチャ構文 である.

auto f() {
  std::vector<int> vec;
  for( int i = 0; i < 100000000; ++i ) {
    vec.push_back( std::rand() );
  }
  return
    [vec = std::move(vec)] (int i) {
      return vec[i];
    };
}
int main()
{
  auto g = f();
  std::cout << sizeof(g) << std::endl;  // sizeof(vector<int>) くらい
}

上記のように, [] の中に 変数名 = 式 のリストをカンマ区切りで書くと,

auto f() {
  std::vector<int> vec;
  for( int i = 0; i < 100000000; ++i ) {
    vec.push_back( std::rand() );
  }
  struct Internal {
    std::vector<int> vec;
    auto operator()(int i) const {
      return vec[i];
    }
  };
  return Internal{ std::move(vec) };
}
int main()
{
  auto g = f();
  std::cout << sizeof(g) << std::endl;  // sizeof(vector<int>)
}

のような感じの処理になる. 変数の型は auto で推論されるものと同じになる.
なお,上記のコードで分かるように,ラムダ式の中で使う変数名は,ラムダ式の外の変数名と同じで良い. ((auto x = std::move(x); とは書けない(厳密に言うなら,書けるけど意図した動作にはならない)のと対照的である.))


これを使えば, std::unique_ptr のような コピーコンストラクタのないクラスも
ラムダ式の内部で使うことができるようになる他,
ローカル変数に対し,手を加えた上でラムダ式にキャプチャさせることもできる.

auto f(int x) {
  return [y = x * 2] (int z) {  // x = x * 2 とも書けるが,紛らわしいので回避したほうが吉
    return y + z;
  };
}

変数名のみを書いたコピーキャプチャと混ぜて使うこともできる.

auto f(int x) {
  return [x, y = x * 2] (int z) {
    return x + y + z;
  };
}

&変数名 = 式 という形で,参照キャプチャを行うこともできる. もちろん寿命には気をつける.

auto f() {
  auto p = std::make_shared<Hoge>();
  return [p, &x = *p] {
    // 処理をここに書く
  };
}

&x = x のような形は,単に &x と書くこともできる.*4

auto f() {
  // std::unique_ptr を使って効率化
  auto p = std::make_unique<Hoge>();
  auto& x = *p;
  return [&x, p = std::move(p)] {  // 寿命問題のため [&x = *p, p = std::move(p)] とは書けない 
    // 処理をここに書く
  };
}

なお,ラムダ式のキャプチャは,書いた順番通りに行われるとは限らないため,
上記のコードで [&x = *p, p = std::move(p)] と書いてしまうと, *p の参照外しに失敗する可能性がある.
それ以外にも,参照は色々と落とし穴があるので,よく分からない方は使わないのが良いだろう.


* * * * *


と,ラムダ式のキャプチャについて,基本的な部分は解説できたことと思う.
これ以外にも, this のキャプチャなど,細かいルールは色々とあるのだが,普通に使う分には関係ないので,
そういう細かいルールは明日,最後の章でまとめて紹介してきたい.


蛇足までに,参照キャプチャとコピーキャプチャが分かりにくいという方のため,簡単なコードを用意してみた.

int main() {
  // ラムダ式の外でローカル変数を変更
  int x = 1, y = 2, z = 3;
  auto f = [&] { return x + y + z; };  // 参照キャプチャ
  int* p = &z;
  auto g = [x, y, z] { return x + y + *p; };  // 値キャプチャ
  // auto g = [x, y, p = &z] {return x + y + *p; }; // こう書くこともできる
  f();  // 6
  g();  // 6
  x = 10;
  f();  // 15; x は変更されている
  g();  // 6; g 内部の x は変更されていない
  x = 1;
  z = 10;
  f();  // 13
  g();  // 13; g 内部の p は z を指している
  p = &x;
  f();  // 13
  g();  // 13; g 内部に保持されている p はコピーなので, z を指したまま

  // ラムダ式内部からローカル変数を変更
  {
    int sum = 0;
    auto acc = [&] (int i) { sum += i; };
    for (int i = 1; i <= 10; ++i) {
      acc(i);
    }
    std::cout << sum << std::endl;  // 55
  }
  {
    int sum = 0;
    auto acc = [sum] (int i) mutable { sum += i; return sum; };
    // もしくは
    // auto acc = [sum = 0] (int i) mutable { sum += i; return sum; };
    for (int i = 1; i <= 10; ++i) {
      acc(i);
    }
    std::cout << sum << std::endl;  // 0
    std::cout << acc(0) << std::endl;  // 55
  }
}

イメージを掴む助けになれば良いのだが.


* * * * *


今回のまとめ:

  • ラムダ式の中ではローカル変数以外は自由に使える. ローカル変数を使いたい場合は,キャプチャをする必要がある
  • [&] は参照キャプチャ. ラムダ式の中からローカル変数を操作できるが,寿命に注意
  • 変数名を [] の中に書くとコピーキャプチャ. 寿命問題を解決できるが効率が悪くなる場合がある
  • コピーキャプチャは std::shared_ptr と組み合わせると便利である
  • C++14 では [x = std::move(x)] のように書くことができる. この場合 x は move される(ムーブキャプチャ)
  • 上記の構文は move 以外でも使うことができ,ローカル変数に対して手を加えた上でキャプチャすることが可能


前回と今回で,ラムダ式の機能はだいたい解説することができた.
次回(最終回)では,これらの機能をどのように使えば効果的なのか,という例を紹介し,
おまけとして,完全解説と銘打つ以上は必要と思われる,マニア向けの細かいルールも紹介したい.
私生活が慌ただしくなってきたため,また時間が空いてしまうかもしれないが,最後まで お読み頂ければ幸いである.

*1:過去の記事 http://d.hatena.ne.jp/gintenlabo/20110121/1295635329 も参考のこと.

*2:グローバル変数の状態変更は,他の関数の内部で行われていた場合,コードから見えにくいので,特に注意が必要である. なお,そういうコードはスレッドセーフでないケースが多いので,参照キャプチャが関わらなくても気をつけた方が良い.

*3:ポインタ変数などを使う場合は その限りではないが,それは C++ の他の機能と同じである.

*4:厳密には その両者は異なるのだが,大雑把に考えるならそう解釈してよい.