2009-11-24

lambda 完全解説

目的

この記事は、C++0xのlambdaを完全に解説せんとする目的を以て書かれた。サンプルコードは最小に留め、エラー処理等は省いた。この記事さえ読めば、今日からlambdaを恐れることなく使う本物のC++0xプログラマになれるだろう。

lambdaとは何ぞや

lambdaである。あるものは、lambda関数、あるいは無名関数という名称を使っている。いったいlambdaとは何なのか。

lambdaは関数である。また、特に名前はない。したがって、lambda関数、無名関数と呼ぶのは、間違ってはいない。しかしここでは、単にlambdaと呼ぶことにする。

lambdaを定義しよう

lambdaは関数オブジェクトである。lambdaは関数オブジェクトである。これはとても大事なので二度書いた。lambdaは関数オブジェクト以外の何物でもない。ところが不思議なことに、皆lambdaが単なる関数オブジェクトであることを承知してくれない。そこで今回は、コードで多くを語りたいと思う。

int main()
{
    [](){} ;
}

このコードは、well-formedなlambdaの最小のコードである。例しにコンパイルしてみるとよい。問題なくコンパイル出来るはずである。[](){} とあるのが、lambdaである。しかし、これはただ、lambdaという関数を定義しているだけなのだ。

lambdaを呼び出そう

lambdaは関数オブジェクトであるので、当然、関数呼び出しができる。関数呼び出しの文法は、通常の関数と同じく、()である。

int main()
{
    [](){}() ;
}

lambdaの文法解説

では、詳しい解説をして行きたいと思う。

int main()
{
    []  //  [ lambda-capture ]
    ()  //  ( parameter-declaration-clause )
    {}  //  compound-statement
    ()  //  Function call expression
    ;
}

まず、一番始めの[]は、lambda-introducerという。[]のなかには、lambda-captureを記述できる。これについては、後に解説する。
二番目は、関数の引数の定義である。通常の関数で、void f(int a, int b) などと書く引数と、まったく同じである。
三番目は、関数の本体である。通常の関数と同じく、実際のコードはこの中に書く。
四番目は、関数呼び出しである。これも、通常の関数とまったく変わらない。

実は、lambdaの引数は省略できるので、本当のlambdaの最小のコードは、以下の通りである。

int main()
{
    []{} ;
}

Hello,World

さて、関数オブジェクトを呼び出すからには、何か意味のあることをさせたい。そこで、lambdaに、Hello,Worldと表示させることにしよう。

int main()
{
  []{ std::cout << "Hello,World" << std::endl ; }() ;
}

そろそろ、lambdaも単なる関数オブジェクトであることが、分かってきたかと思う。

変数に代入

lambdaは関数オブジェクトである。これを承知できないのは、文法が少し変わっているからに過ぎないのだ。lambdaは関数オブジェクトであるが故に、変数に代入できる。

int main()
{
  // 変数へ代入
  auto func = []{ std::cout << "My hovercraft is full of eels." << std::endl ; } ;

  // 呼び出し
  func() ;
}

関数の引数に渡す

また、別の関数に渡せる。

template < typename Func >
void f( Func func )
{
  func() ;
}

int main()
{
  f( []{std::cout << "All your lambda is belong to us." << std::endl ; } ) ;
}

これで諸君も、否応なくlambdaが関数オブジェクトであることを認識できたであろう。

引数を取る

lambdaは関数オブジェクトであるので、引数を取れる。

int main()
{
  [](std::string const & str)          // 引数
  { std::cout << str << std::endl ; }   // 関数本体
  ("Have you read your SICP today?") ;  // 関数呼び出し
}

戻り値を返す

lambdaは関数オブジェクトであるので、戻り値を返せる。

int main()
{
  // 戻り値は、明示的に書かなくても推測してくれる
  auto a = []{ return 0 ; }() ;

  // 戻り値を明示的に書いた場合。
    // doubleからfloatへの型変換をしている。
  auto b = []() -> float { return 3.14 ; }() ; 
}

この、->、という記述は、少し戸惑うかもしれないが、こういう文法になっているので、仕方がないのである。

lambda関数では、戻り値は明示的に書かなくても、推測してくれる。ただし、{ return expression ; }の形でなければならない。この形でない場合は、void型を返すと見なされる。戻り値を明示する場合は、たとえ引数を取らないlambdaでも、引数リストを省略することは出来ない。

変数のキャプチャ

さて、ここまで読み進めれば、lambdaが関数オブジェクトであることは、疑いようもなく理解できたと思う。しかし、ここで一つ疑問がある。「なぜlambdaなのだ。なぜもっと他の、分かりやすい名称ではないのだ」と。もっともな疑問である。lambdaをlambdaたらしめる、最も重要な機能を解説しよう。

lambdaは、その定義されている関数のスコープ内の変数を、キャプチャできる。これも、文章で説明するのは分かりにくい。例を示す。

int main()
{
  std::string x = "I'm a lumberjack and I'm OK." ;

  // 参照によるキャプチャ
  [&]{ std::cout << x << std::endl ; }() ;
  
  // コピーによるキャプチャ
  [=]{ std::cout << x << std::endl ; }() ;
}

キャプチャには二種類ある。参照によるキャプチャと、コピーによるキャプチャである。参照というのは、lambdaのオブジェクト内で、変数の参照を保持するものである。コピーとは、lambdaのオブジェクト内で、変数そのものをコピーして保持するものである。その結果、lambdaが定義されている関数内のスコープにある変数を、lambdaの中で使うことが出来る。

例えば、以下のように使える。

template < typename Func >
void f( Func func )
{
  func(" and I'm OK.") ;
}


int main()
{
  std::string x = "I'm a lumberjack" ;

  f( [&](std::string const & str){ x += str ;} ) ;

    // 変数が書き換わっている。
    // "I'm a lumberjack and I'm OK."
  std::cout << x << std::endl ;
}

コピーの場合はどうだろうか。

template < typename Func >
void f( Func func )
{
  func(" and I'm OK.") ;
}


int main()
{
  std::string x = "I'm a lumberjack" ;

    // Error
    f( [=](std::string const & str){ x += str ;} ) ;

  std::cout << x << std::endl ;
}

コピーの場合は、エラーになってしまう。何故ならば、lambdaの関数呼び出し演算子は、const修飾されているからだ。もし、どうしてもコピーのキャプチャで、変数を書き換えたいのならば、mutableが使える。

template < typename Func >
void f( Func func )
{
  func(" and I'm OK.") ;
}


int main()
{
  std::string x = "I'm a lumberjack" ;

  f( [=](std::string const & str) mutable { x += str ;} ) ;

    // コピーなので、変数は書き換わっていない。
  // "I'm a lumberjack"
  std::cout << x << std::endl ;
}

mutableは、引数リストの後、戻り値指定の前に記述する。

[]() mutable -> {} ;

キャプチャを、コピーか参照のどちらでするかは、個々に指定できる。

int main()
{
  int a = 0, b = 0 ;

  [a, &b]() mutable { a = 1 ; b = 1 ; }() ;

  // 0
  std::cout << a << std::endl ;
  // 1
  std::cout << b << std::endl ;
}

このように、変数を列挙すればいい。また、一部の変数だけを指定して、残りはひとまとめに指定したい場合、capture-defaultを指定すればよい。

int main()
{
  int a, b, c, d, e, f, g ;

  // a, bのみコピー、その他は参照
  [&, a, b] { } ;

  // a, bのみ参照、その他はコピー
  [=, &a, &b] { } ;

}

ただし、デフォルトのキャプチャと同じものを指定することは出来ない。

int main()
{
  int a, b ;

  // Error. デフォルトと同じ
  [&, &a] { } ;

  // Error. デフォルトと同じ
  [=, a] { } ;

}

同じ変数を複数書くことは出来ない

int main()
{
  int a;

  // Error. aを二度書いている。
  [a, a] { } ;
}

this

クラスの非静的なメンバ関数内のlambdaで、thisを使った場合、そのクラスのオブジェクトのthisになる。

struct X
{
  int a ;
  void f()
  {
    [=]{ this->a = 1 ;}() ;
  }
} ;

thisはポインタであるので、この場合、キャプチャがコピーでも参照でも、aは書き換わる。

lambdaを返す関数

lambdaは関数の戻り値として返すことも出来る。

std::function< void () > f()
{
  std::string str("hello") ;
  return [=]{ std::cout << str << std::endl ; } ;
}

int main()
{

  // 一度変数に代入してから呼び出す。
  auto func = f() ;
  func() ;

  // lambdaを変数に代入せずそのまま呼び出す。
  f()() ;

}

このように、C++0xで追加された、すばらしい標準ライブラリ、std::functionを使えば、lambdaを返すことが出来る。

キャプチャが参照ではなくコピーであることに注意されたい。f()が戻る時点で、strは破棄されるので、ここはコピーでなくてはならない。

lambdaの型

規格では、lambdaは、ユニークな型を持つと定義されている。ただし、以下のコードはエラーである。

// Error
decltype([]{}) ;

これを出来るようにすると、コンパイラの実装が難しくなるからだ。同じ理由で、sizeofも使えない。

lambda実践

コードは文章よりも分かりやすい。

struct X
{
  int value ;
} ;

int main()
{
  std::vector< X > v(20) ;

  std::mt19937 rng ;
  std::uniform_int_distribution<int> dist(1, 99) ;

  // 乱数で初期化
  std::generate( v.begin(), v.end(),
    [&]() -> X {
      X x ;
      x.value = dist( rng ) ;
      return x ;
    }
  ) ;

  // 表示
  std::for_each( v.begin(), v.end(),
    [](X const & x)
            { std::cout << x.value << " " ; }
  ) ;
  std::cout << std::endl ;

  // ソート
  std::sort( v.begin(), v.end(),
    [](X const & a, X const & b)
            { return a.value < b.value ; }
  ) ;

  // 表示
  std::for_each( v.begin(), v.end(),
    [](X const & x){ std::cout << x.value << " " ; }
  ) ;
  std::cout << std::endl ;
}

最後に

結局の所、lambdaとは、ちょっと便利な関数オブジェクトに過ぎないのである。C++で関数オブジェクト使うのは常識であるから、C++0xでlambdaを使うのも常識になるだろう。

2 comments:

Anonymous said...

初心者なので大変参考になりました。

表示、ソートの箇所で書かれている
[](X const & x)

[](const X& x)
のタイプミスでしょうか?

Anonymous said...

あきれた初心者はコンパイルもせずにタイプミスなどという失礼な言葉を打つ