WonderPlanet DEVELOPER BLOG

ワンダープラネットの開発者ブログです。モバイルゲーム開発情報を発信。

C++11とラムダ式のあれこれ

こんにちは。今回ブログを担当する長屋です。

さてCocos2d-x v3.0からC++11の機能が使用できるようになりました。
その中で使用頻度が高いラムダ式を掘り下げてみたいと思います。

C++の場合、ラムダ式は簡潔に関数オブジェクトを生成する機能となっています。

〜以下のように構成されています〜
[キャプチャ](仮引数リスト) -> 戻り値の型 { ラムダ式の本文 }

キャプチャとは?
ラムダ式外で宣言された参照したいオブジェクトとそのオブジェクトの渡し方を設定できます。
&」演算子や「=」演算子を使用することで設定することができます。

〜キャプチャパターンの例〜

[&val] valの参照を渡します。
[val] valのコピーを渡します。
[&val01,val02] val01の参照とval02のコピーを渡します。
[&] 全てのオブジェクトの参照を渡すことができます。 メンバ関数内での宣言の場合はthisポインタもキャプチャされます。 ラムダ式内部でメンバ変数を呼んだ場合、暗黙的にthisポインタを経由して呼ばれます。
[=] 全てのオブジェクトをコピーで渡すことができます。 メンバ関数内での宣言の場合はthisポインタもキャプチャされます。 ラムダ式内部でメンバ変数を呼んだ場合、暗黙的にthisポインタを経由して呼ばれます。
[] 設定を省略することもできます。 省略した場合はラムダ式が定義されているスコープの変数を呼ぶことはでません。 ただし定数やグローバル変数、静的変数などは呼ぶことができます。 定数は呼べるのでローカルで設定した「constexpr」で定数化した変数は呼べます。

#include <iostream>  
  
void test01() {  
    int val01 = 1;  
    int val02 = 10;  
  
    //「&」演算子を使用して参照を渡したいオブジェクトを設定します。  
    // ラムダ式の定義の後に「()」演算子をつけることでその場で実行できます。  
    [&val01]() -> void { val01 *= 2; }();  
  
    //「2」  
    std::cout << val01 << std::endl;  
  
    // 何も演算子を指定していない場合はコピーされた値が渡されます  
    // 戻り値の型をint型に設定しているためint型の値が帰ってきます。  
    val02 = [val02]() -> int { return val02 * 2; }();  
  
    //「20」  
    std::cout << val02 << std::endl;  
  
    //複数設定も可能  
    [&val01,val02]() -> void { val01 *= val02; }();  
  
    //「40」  
    std::cout << val01 << std::endl;  
  
    //変数を指定しない場合は全ての変数をコピーで取得できる  
    int total = [=]() -> int {return val01 + val02;}();  
  
    // 「120」  
    std::cout << total << std::endl;  
  
    // キャプチャ設定をしていないため不可  
    // int total02 = []() -> int {return val01 + val02;}();  
  
    //これはok  
    int res = []() -> int { return 100; }();  
  
    //「100」  
    std::cout << res <<std::endl;  
  
    //定数設定  
    constexpr int val03 = 200;  
  
    //これもOK  
    // 「200」  
    []() -> void { std::cout << val03 << std::endl; }();  
}  

コピーキャプチャで変数を渡した場合は暗黙的にconst修飾されます
コピーキャプチャされた変数にどうしても変更を加えたいときは「mutable」を仮引数リストに続いて指定しましょう。

struct Hoge { int val = 2; };  
  
void test02() {  
    int val = 10;  
    Hoge hoge;  
  
    //NG  
    //[val](){ val = 100; }();  
    //[=](){ hoge.val = 100; }();  
  
    //OK  
    [val]() mutable { val = 100; }();  
    [=]() mutable { hoge.val = 100; }();  
}  

☆仮引数リスト
仮引数リストにはラムダ式に対して渡したい引数を設定することができます。

#include <iostream>  
  
void test03() {  
    int val = 100;  
    int total = [](int x) -> int { return 1 + x;}(val);  
  
    //「101」  
    std::cout << total <<std::endl;  
  
    //初期化リストも使えます  
    auto cat = [](std::initializer_list<std::string> list) {  
        for (auto str : list) {  
            std::cout << str << std::endl;  
        }  
    };  
  
    //「nya」  
    //「nyan」  
    //「nya-」  
    cat({"nya","nyan","nya-"});  
}  

☆表記の省略

戻り値の表記や仮引数リストは省略することもできます。

void test04() {  
    //OK  
    int res = []{ return 100; }();  
  
    //最短のラムダ式  
    // 引数なし,戻り値なしの何もしないラムダ式  
    []{}();  
}  

☆ラムダ式の代入
ラムダ式は内部的に関数オブジェクトを生成しているので関数オブジェクトに対して代入することができます。

#include <iostream>  
#include <functional>  
using namespace std;  
  
void test05() {  
    int val = 10;  
  
    //戻り値がない場合はvoidで設定  
    function<void()> hellow = []{ cout << "Hellow!Lamda!" << endl; };  
  
    //関数引数と戻り値にあったオブジェクトを設定  
    function<int(int x)> cal01 = [=](int x){ return val * x;};  
  
    //auto型で受け取ることももちろん可能  
    auto cal02 = [&](){ return val * val;};  
  
    //「Hellow!Lamda!」  
    hellow();  
  
    // 「50」  
    cout << cal01(5) << endl;  
  
    // 「100」  
    cout << cal02() << endl;  
  
    //配列に登録して一気に実行  
    vector<function<void()>> funcList;  
  
    funcList.push_back([]{cout << "nya" << endl;});  
    funcList.push_back([]{cout << "nyan" << endl;});  
    funcList.push_back([]{cout << "nya-" << endl;});  
    funcList.push_back(hellow);  
  
    //「nya」  
    //「nyan」  
    //「nya-」  
    //「Hellow!Lamda!」  
    for (auto &v : funcList) {  
        v();  
    }  
  
    //キャプチャを省略しているラムダ式に限り、関数ポインタへの代入も可能です  
    void (*hellow_ptr)() = []{ cout << "Hellow!func_ptr!" << endl; };  
    int (*cal03_ptr)(int) = [](int x){ return x * x; };  
  
    //「Hellow!func_ptr!」  
    hellow_ptr();  
  
    // 「100」  
    cout << cal03_ptr(10) << endl;  
}  
  

少し脱線しますがC++11ではラムダ式にテンプレート引数を持たせることはできません。
しかしC++14からauto型で引数をきにせずにラムダ式を作ることができます。
また可変長引数も持たせる事ができます。

#include <iostream>  
using namespace std;  
  
void print (){  
     cout << endl;  
}  
  
template <typename T,typename ...Types>  
void print (T head, Types... tail) {  
    cout << head << endl;  
    print(tail...);  
}  
  
void test06() {  
    // auto型で仮引数を設定  
    auto add = [](auto x){return x + x;};  
  
    // 「8」  
    cout << add(4) << endl;  
  
    // 「6.4」  
    cout << add(3.2) << endl;  
  
    // 「abcabc」  
    cout << add(std::string("abc")) << endl;  
  
    //可変長引数も可能  
    //可変長引数の場合はC++11の可変長テンプレートが使用される。  
    auto cat = [](auto head,auto...tail) {  
        //パラメーターパックを再帰的に展開  
        print(head,tail...);  
    };  
  
    // 「1」  
    // 「3」  
    // 「4」  
    // 「nya」  
    // 「2.12」  
    // 「nya-」  
    cat(1,3,4,"nya",2.12,"nya-");  
  
    // 範囲ベースでパラメーターパックを展開  
    auto cat02 = [](auto ... nya) {  
        for (auto nya_ : {nya...}) {  
            cout << nya_;  
        }  
    };  
  
    // 「nyanyannya-」  
    cat02("nya","nyan","nya-");  
}  

備忘記録も兼ねてまとめてみました。
関数オブジェクトが使用されているところは全てラムダ式と差し替えることができます。
Cocos2d-xでも使用頻度が高いためどんどん活用していきたいです。