C++11のラムダ関数の簡単なまとめ


このエントリーをはてなブックマークに追加

C++11で導入されたラムダ関数を使えるようになるための簡単な覚書です。

ラムダ関数とは?

名前のない関数のことです。無名関数と呼ばれることもあります。 わざわざ関数を定義するほどのこともない、ちょっとした処理を行いたいときに使われることが多いです。あまりラムダ関数で複雑な処理をするとコードが読みにくくなるので注意。(関数に名前がついていないので処理の内容を予想できなくなるため)

最初の例

まず最初に、「整数を一つとり、その二乗を返すラムダ関数」から。このラムダ関数は以下のように書けます。

auto f1 = [](int x) { return x * x; };

このラムダ関数を呼び出してみます。

// ラムダ関数を呼び出す。関数ポインタみたいに使える
std::cout << "1: " << f1(1) << std::endl;
std::cout << "3: " << f1(3) << std::endl;
std::cout << "5: " << f1(5) << std::endl;

以下のように出力されます。

1: 1
3: 9
5: 25

同じ調子で、「整数を二つとり、その和を返すラムダ関数」を定義して呼び出してみます。

auto f2 = [](int x, int y) { return x + y; };
std::cout << "1 + 4: " << f2(1, 4) << std::endl;
std::cout << "8 + 9: " << f2(8, 9) << std::endl;
1 + 4: 5
8 + 9: 17

戻り値の型の指定

戻り値の型を指定するには以下のようにします。

auto f1_double = [](int x) -> double { return x * x; };
std::cout << "10000: " << f1_double(10000) << std::endl;  // "10000: 1e+08"と出力される

引数の省略

引数がない場合は丸括弧を省略できます。しかし個人的には、括弧があったほうが一見してラムダ関数だと認識しやすく感じるので、省略しない方がよい気がします。

auto f_abbrv = []{ std::cout << "hello!" << std::endl; };
f_abbrv();

キャプチャ

以下の例はビルドが通りません。ラムダ関数の外にある変数a, bを使っているからです。

int a = 3;
int b = 7;

// error! 外側で定義された変数はそのままでは使えない
auto f_offset = [](int n){ return a * n + b; };

これを解決するためにはキャプチャを使います。角括弧の中にコピーしたい変数を羅列します。

auto f_offset = [a, b](int n){ return a * n + b; };
std::cout << "a * 5 + b = " << f_offset(5) << std::endl;

ちなみに上の例で[a,&b]と書くと、bの値を書き換えることも可能になります。

変数を羅列するのが面倒なら=と書きます。すべての外側の変数がリードオンリーで見えるようになります。

auto f_offset2 = [=](int n){ return a * n + b; };

さらに、すべての外側の変数が書き換え可能状態で見えるようにしたいなら&を使います。スペルミスにより予期せぬ動作が起こりえるので使用には注意が必要です。

auto f_offset2 = [=](int n){ return a * n + b; };

応用例1: ソート

ラムダ関数の応用例の一つはソートです。C++で構造体をソートする4つの方法(おまけあり) - minus9d's diaryの「方法2」で、構造体を格納したvectorを、比較関数を使ってソートする例を書きました。この比較関数の部分にラムダ関数を使うことができます。

以下の例では、構造体を以下のポリシーでソートしています。

  • 数字numが小さい順にソート
  • もし数字numが同じなら、文字列strが若い順にソート
struct data_t {
    int num;
    std::string str;
};

std::vector<data_t> data_array(3);

data_array[0].num = 15;
data_array[0].str = "zzz";
data_array[1].num = 30;
data_array[1].str = "aaa";
data_array[2].num = 15;
data_array[2].str = "ccc";

// 並び替えの方法をラムダ関数で記述
sort(data_array.begin(),
     data_array.end(),
     [](const data_t& a, const data_t& b){return (a.num == b.num) ? (a.str < b.str) : (a.num < b.num);}
    );

// 結果を確認
for(int i = 0; i < (int)data_array.size(); ++i){
    std::cout << "i " << i << ": " << std::endl;
    std::cout << "  num: " << data_array[i].num << std::endl;
    std::cout << "  str: " << data_array[i].str << std::endl;
}        

以下は出力結果です。意図通りソートできています。

i 0:
  num: 15
  str: ccc
i 1:
  num: 15
  str: zzz
i 2:
  num: 30
  str: aaa

応用例2: for_each

もう一つの応用例は、<function>ヘッダで提供されるfor_each()です。これを使うと、vectorの各要素への一律な処理が簡潔に書けます。

以下の例では、vectorの各要素を2乗しています。

std::vector<int> vec{2, 3, 5, 7, 11};

// &を使うことで値を書き換え可能
std::for_each(vec.begin(), vec.end(), [](int &n){ n = n * n; });

コード全文

今回の記事内容を試すためのコードの全文です。

//
// ラムダ式の学習用サンプル
//
// 参考URLs
//   http://www.cprogramming.com/c++11/c++11-lambda-closures.html
//   http://wwweic.eri.u-tokyo.ac.jp/computer/manual/altixuv/doc/Compiler/CC/jp_lnx_cppdocs11.1.tar/Documentation/ja_JP/compiler_c/main_cls/cref_cls/common/cppref_lambda_lambdacapt.htm
//   http://handasse.blogspot.com/2010/01/c0x-stl.html
//   http://kaworu.jpn.org/cpp/%E3%83%A9%E3%83%A0%E3%83%80%E5%BC%8F


#include <iostream>
#include <vector>
#include <string>
#include <algorithm>

void basic(){
    // 整数を一つとり、その二乗を返すラムダ関数を定義
    auto f1 = [](int x) { return x * x; };

    // ラムダ関数を呼び出す。関数ポインタみたいに使える
    std::cout << "1: " << f1(1) << std::endl;
    std::cout << "3: " << f1(3) << std::endl;
    std::cout << "5: " << f1(5) << std::endl;
    // 整数を一つとり、その二乗を返すラムダ式を定義

    // 整数を二つとり、その和を返すラムダ関数を定義
    auto f2 = [](int x, int y) { return x + y; };
    // ラムダ関数を呼び出す
    std::cout << "1 + 4: " << f2(1, 4) << std::endl;
    std::cout << "8 + 9: " << f2(8, 9) << std::endl;

    // 戻り値の型を明示してもよい
    auto f1_double = [](int x) -> double { return x * x; };
    std::cout << "10000: " << f1_double(10000) << std::endl;
    
    // 引数をとらない場合()は省略できる
    // しかし省略しない方が見やすいかも?
    auto f_abbrv = []{ std::cout << "hello!" << std::endl; };
    f_abbrv();
}

// captureの例
void capture(){
    int a = 3;
    int b = 7;

    // error! 外側で定義された変数はそのままでは使えない
    // auto f_offset = [](int n){ return a * n + b; };
    
    // []の中に、コピーしたい変数を羅列すると、その変数がリードオンリーで見える
    auto f_offset = [a, b](int n){ return a * n + b; };
    std::cout << "a * 5 + b = " << f_offset(5) << std::endl;

    // []の中に=と書くと、すべての外側の変数がリードオンリーで見える
    auto f_offset2 = [=](int n){ return a * n + b; };
    std::cout << "a * 5 + b = " << f_offset2(5) << std::endl;

    // []の中に&と書くと、すべての外側の変数が書き換え可能状態で見える
    auto f_offset3 = [&](int n){ a = 100; b = 77; return a * n + b; };
    std::cout << "a * 5 + b = " << f_offset3(5) << std::endl;
}

// 並び替えの例
void applicationSort(){
    struct data_t {
        int num;
        std::string str;
    };
        
    std::vector<data_t> data_array(3);

    data_array[0].num = 15;
    data_array[0].str = "zzz";
    data_array[1].num = 30;
    data_array[1].str = "aaa";
    data_array[2].num = 15;
    data_array[2].str = "ccc";

    // 並び替えの方法をラムダ関数で記述
    sort(data_array.begin(),
         data_array.end(),
         [](const data_t& a, const data_t& b){return (a.num == b.num) ? (a.str < b.str) : (a.num < b.num);}
        );

    // 結果を確認
    for(int i = 0; i < (int)data_array.size(); ++i){
        std::cout << "i " << i << ": " << std::endl;
        std::cout << "  num: " << data_array[i].num << std::endl;
        std::cout << "  str: " << data_array[i].str << std::endl;
    }        
}

// foreachの例
void applicationForEach(){    
    std::vector<int> vec{2, 3, 5, 7, 11};

    // before
    for (auto n: vec)
    {
        std::cout << n << ", ";
    }
    std::cout << std::endl;

    // &を使うことで値を書き換え可能
    std::for_each(vec.begin(), vec.end(), [](int &n){ n = n * n; });

    // after
    for (auto n: vec)
    {
        std::cout << n << ", ";
    }
    std::cout << std::endl;
}

int main(){
    basic();
    capture();
    applicationSort();
    applicationForEach();
    return 0;
}