lambda 表达式


C++ 11 可以使用 lambda 表达式,可以在函数中封装一段演算流程进行传递,例如,在〈函数指针〉的范例中,定义了ascendingdescending函数以便传递,如果事先这两个函数并不存在,你想在main直接传递比序演算,C++ 11 以后可以如下:

#include <iostream> 
#include <functional>
#include <algorithm>
using namespace std; 

int main() { 
    int number[] = {3, 5, 1, 6, 9};
    auto print = [](int n) { cout << n << " "; };

    sort(begin(number), end(number), [](int n1, int n2) { return n2 - n1; });
    // 显示 9 6 1 5 3
    for_each(begin(number), end(number), print);
    cout << endl;

    sort(begin(number), end(number), [](int n1, int n2) { return n1 - n2; });
    // 显示 3 5 1 6 9
    for_each(begin(number), end(number), print);
    cout << endl;

    return 0; 
}

在上头你看到了几个[]开头的表达式,这些表达式是 lambda 表达式,你也看到了sortfor_each,这些是定义在algorithm的函数,可以给它数组开头与结尾的地址,并传递一段演算,声明想对数组做些什么,sort是指定了比序的依据,而for_each指定了print定义的演算,也就是接受数组元素值并显示在标准输出。

lambda 表达式定义了一个Callable对象,也就是个可以接受调用操作的对象,例如函数就是其中之一。来看看 lambda 表达式的定义方式:

[ captures ] ( params ) -> ret { body }
[ captures ] ( params ) { body }
[ captures ] { body }

简单来说,( params ) -> ret可以依需求编写,来看看方才范例中的print

auto print = [](int n) { cout << n << " "; };

这定义了一个 Callable 对象,调用时可以接受一个实参,因为没有return,也没有定义 lambda 表达式的返回类型,就自动推断为ret的部份为void,也就是相当于:

auto print = [](int n) -> void { cout << n << " "; };

那么print的类型是什么呢?lambda 表达式会创建一个匿名类(称为 closure type)的实例,因为无法获取匿名类的名称,也就无法定义其类型,因而大多使用auto来自动推断。

然而这就有一个问题,若要定义一个函数可以接受 lambda 表达式,参数无法使用auto,怎么办呢?可以包含functional标头文件,使用function来定义,function的实例可以接受 Callable 对象,lambda 表达式是其中之一,例如:

function<void(int)> print = [](int n) { cout << n << " "; };

若 lambda 表达式被指定给函数指针,那么 lambda 表达式创建的实例会转换为地址:

void (*f)(int) = [](int n) { cout << n << " "; };

因此,既有的函数若参数是函数指针类型,也可以接受 lambda 表达式。

lambda 表达式的本体若有return,然而没有定义ret的类型时,会自动推断,因此底下f1ret类型会自动推断为int

auto f1 = [](int n1, int n2) { return n2 - n1; }
auto f2 = [](int n1, int n2) -> int { return n2 - n1; };

接下来看[capture],在若只定义为[]时,没办法使用任何 lambda 表达式外部的变量,若想运用外部变量,定义时基本上从=&出发:

  • [=]:lambda 表达式本体可以取用外部变量。
  • [&]:lambda 表达式本体可以参考外部变量。

使用=时,lambda 表达式本体中取用到某外部变量时,其实是隐含地创建了同名、同类型的局部变量,然后将外部变量的值复制给局部变量,默认情况下不能修改,然而可以加上mutable修饰,不过要注意的是,这时修改的会是局部变量的值,不是外部变量。例如:

#include <iostream> 
using namespace std; 

int main() { 
    int x = 10;

    auto f = [=]() mutable -> void {
        x = 20;
        cout << x << endl;
    };

    f(); // 显示 20
    cout << x << endl; // 显示 10

    return 0; 
}

使用=时,lambda 表达式本体中参考外部变量时,其实是隐含地创建了同名的参考,因此在 lambda 表达式本体中修改变量,另一变量取值就也会是修改过的结果:

#include <iostream> 
using namespace std; 

int main() { 
    int x = 10;

    auto f = [&]() mutable -> void {
        x = 20;
        cout << x << endl;
    };

    f(); // 显示 20
    cout << x << endl; // 显示 20

    return 0; 
}

[capture]可以限定捕捉的变量有哪些,以及以哪种方式捕捉:

  • [x, y]:以 = 的方式取用外部的 x、y。
  • [x, &y]:以 = 取用外部的 x,以 & 的方式参考外部的 y。
  • [=, &y]:以 & 的方式参考外部的 y,其余外部变量取用时都是 = 的方式。
  • [&, y]:以 = 的方式参考外部的 y,其余外部变量以 & 的方式参考。

要设置默认捕捉方式时,对于没指定捕捉方式的其他变量,就会采用默认捕捉方式。

若有必要,lambda 表达式创建之后也可以马上调用,例如:

#include <iostream> 
using namespace std; 

int main() { 
    // 显示 Hello, Justin
    [](const char *name) {
        cout << "Hello, " << name << endl;
    }("Justin");
    return 0; 
}

在定义模版(template)时,lambda 表达式也可以模版化。例如:

template <typename T>
function<T(T)> negate_all(T t1) {
    return [=](T t2) -> T {
        return t1 + t2;
    };
}

在 C++ 14,捕捉变量时,可以创建新变量并指定其值,新变量的类型会自动推断。例如:

auto print = [x = 10](int n) { cout << n + x << " "; };

虽然函数的参数类型不能以auto定义,然而在 C++ 14,lambda 表达式的参数类型可以是auto

#include <iostream> 
using namespace std; 

int main() { 
    auto plus = [] (auto a, auto b) {
        return a + b;
    };

    // 显示 3
    cout << plus(1, 2) << endl; 

    // 显示 abcxyz
    cout << plus(string("abc"), string("xyz")) << endl;

    return 0; 
}

指定给plus的 lambda 表达式,称为泛型 lambda(generic lambda),原理是基于模版,实参类型只要符合本体中的实现协定就可以用来调用 lambda 表达式,在上例中就是实参要能使用+运算符处理。


展开阅读全文