C++11 泛型算法之Lambda表达式

1 什么是Lambda表达式?

  首先,我们理解一下函数,函数是什么?函数就是一个命名了的代码块,我们通过调用函数来执行相应的代码。那什么是Lambda 表达式呢?Lambda 表达式(lambda expression)也被称为匿名函数,即没有函数名的函数。

  与函数类似,一个lambda有一个返回类型、一个参数列表和一个函数体;与函数不同,lambda可能定义在函数内部,且必须使用尾置返回来指定返回类型(尾置返回看不懂没关系,后面会说)

2 怎么写Lambda表达式?

  一个lambda表达式具有如下形式:

[capture list] (parameter list) -> return type { function body }
  • capture list   :捕获列表
  • parameter list :参数列表
  • return type     :返回类型
  • function body :函数体

当然,我们可以忽略参数列表返回类型,但必须永远包含捕获列表函数体

auto f = [] { return 42; }

Lambda的调用方式与普通函数的调用方式相同,都是使用调用运算符( () )。

cout << f() << endl;

2.1 捕获列表

  首先,我们需要知道捕获列表有什么用呢?当一个lambda表达式出现在一个函数中, 如果需要使用函数中的局部变量(非static变量),需要将局部变量包含在其捕获列表中。如下例所示。

// 获取一个迭代器,指向第一个满足size()>=6的元素 
auto A()
{
    int sz = 6;
    static int static_sz = 6;
    vector<string> words = {"apple","banana","orange"};

    // 正确:sz已捕获
    auto we = find_if(words.begin(), words.end(), [sz] (const string &a) { return a.size()>= sz; }); 

    // 错误:sz未捕获
    auto we1 = find_if(words.begin(), words.end(), [] (const string &a) { return a.size()>= sz; }); 

    // 错误:无法在lambda中捕获带有静态储存持续时间的变量
    auto we2 = find_if(words.begin(), words.end(), [static_sz ] (const string &a) { return a.size()>= static_sz ; }); 

    return we;
}

那么,我们应该怎么写捕获列表,捕获列表有哪些表现形式,分别表示什么含义呢?下表展示了捕获列表的各种形式及其对应的含义。

lambda捕获列表形式总览表
空捕获 [] 空捕获列表。lambda不能使用所在函数中的变量。
显式捕获:值捕获 [A,B,...] A,B等名字都是lambda所在函数的局部变量。该形式下,捕获列表中的变量都被拷贝,属于值捕获。
显式捕获:引用捕获 [&A,&B,...] A,B等名字都是lambda所在函数的局部变量。该形式下,采用的是引用捕获方式。
隐式捕获:引用捕获 [&] 隐式捕获列表,采用引用捕获方式。lambda体中所使用的来自所在函数的实体都采用引用方式使用
隐式捕获:值捕获 [=] 隐式捕获列表,采用值捕获方式。lambda体将拷贝所使用的来自所在函数的实体的值
混合使用:隐式引用捕获和显式值捕获 [&,identifier_list] identifier_list是一个逗号分隔的列表,包含0个或多个来自所在函数的变量。这些变量采用值捕获方式,而任何隐式捕获的变量都采用引用方式捕获。identifier_list中的名字前而不能使用&
混合使用:隐式值捕获和显式引用捕获 [=,identifier_list] identifier_list中的变量都采用引用方式捕获,而任何隐式捕获的变量都采用值方式捕获。identifier_list中的名字不能包括this,且这些名字之前必须使用&

 

2.1.1 空捕获

  空捕获是指lambda表达式的捕获列表为空,即未捕获任何变量。如下例所示,lambda表达式f的函数体内部只有一条return语句,不需要用到fcn1函数中的任何变量,所以捕获列表为空。

void fcn1()
{
    // 捕获列表为空
    auto f = [] { return 42; }
    cout << f() << endl;
}

2.1.2 显示捕获

(1)值捕获

  与传值参数类似,采用值捕获的前提是变量可以拷贝。与参数不同,被捕获的变量的值是在Lambda创建时拷贝,而不是调用时拷贝:

void fcn2()
{
    size t v1 = 42; // 局部变量
    // 值捕获:将v1拷贝到名为f的可调用对象
    auto f = [v1] { return vl; }; 
    v1= 0; 
    auto j = f () ; // j为42;f保存了我们创建它时v1的拷贝
}

(2)引用捕获

void fcn3()
{
    size t v1 = 42; // 局部变量
    // 对象f2包含v1的引用
    auto f2 = [&v1] { return v1; }; 
    v1 = 0; 
    auto j = f2 () ; // j为0;f2保存v1的引用,而非拷贝
}

  引用捕获返回引用有着相同的问题与限制。在以引用方式捕获一个变量时,必须保证在lambda执行时变量是存在的。lambda捕获的都是局部变量,这些变量在函数结束后就不复存在了。如果lambda可能在函数结束后执行,捕获的引用指向的局部变量就己经消失了。

void biggies(vector<string> &words,vector<string>::size_type sz, ostream &os = cout, char c = ' ')
{
    // 依次打印到os 调用for_each时,Lambda会立即执行。在此情况下,以引用方式捕获os没有问题
    for_each(words.begin(), words.end(), [&os, c](const string &s) ( os << s << c;});
}

2.1.3 隐式捕获

  捕获除了上述显式列出我们希望使用的来自所在函数的变量之外,还可以让编译器根据lambda函数体中的代码来推断我们要使用哪些变量。为了指示编译器推断捕获列表,应在捕获列表中写一个"&"或"="。"&"告诉编译器采用捕获引用方式,而"="表示采用值捕获方式。以下示例,用隐式捕获重写上述显示捕获示例。

(1)值捕获

void fcn2()
{
    size t v1 = 42; // 局部变量
    // 隐式捕获:值捕获
    auto f = [=] { return v1; }; 
    v1= 0; 
    auto j = f () ; // j为42;f保存了我们创建它时v1的拷贝
}

(2)引用捕获

void fcn3()
{
    size t v1 = 42; // 局部变量
    // 对象f2包含v1的引用
    auto f2 = [&] { return v1; }; 
    v1 = 0; 
    auto j = f2 () ; // j为0
}

2.1.4 混合使用显示捕获与隐式捕获

  如果我们希望对一部分变量采用值捕获,对其他变量采用引用捕获, 可以混合使用隐式捕获和显式捕获。当我们混合使用隐式捕获和显式捕获时,捕获列表中的第一个元素必须是一个"&"或"="。

  当混合使用隐式捕获和显式捕获时,显式捕获的变量必须使用与隐式捕获不同的方式。 即,如果隐式捕获是引用方式(使用了"&"), 则显式捕获命名变量必须采用值方式, 因此不能在其名字前使用"&"。类似的, 如果隐式捕获采用的是值方式(使用了"="), 则显式捕获命名变量必须采用引用方式, 即,在名字前使用"&"。

void biggies(vector<string> &words,vector<string>::size_type sz, ostream &os = cout, char c = ' ')
{
    // os隐式捕获,引用捕获方式;c显式捕获,值捕获方式
    for_each(words.begin(), words.end(), [&,c] (const string &s) { os << s << c; }) ; 
    // os显式捕获,引用捕获方式;c隐式捕获,值捕获方式
    for_each(words.begin(), words.end(), [=,&os] (const string &s) { os << s << c; }) ; 
}

2.2 参数列表

  lambda不能有默认参数。因此,一个lambda表达式调用的实参数目永远与形参数目相等。

// 带有参数列表的Lambda表达式
[] (const string &a, const string &b) 
    {return a.size() < b.size();} 

2.3 返回类型

2.3.1 忽略返回类型

  如果像如下代码所示,忽略lambda表达式的返回类型,即省略了"-> return type"。lambda表达式可以根据函数体中的代码推断出返回类型。前提是函数体只是一个return语句,返回类型从返回表达式的类型推断而来

// 可推断返回类型为int
transform(vi.begin(),vi.end(),vi.begin(), [] (int i) {return i < 0 ? -i : i;});

如果lambda的函数体包含任何单一return语句以外的内容,且未指定返回类型,则默认返回void。

// 错误:编译器推断这个版本的lambda返回类型为void,但它却返回了一个int
transform(vi.begin(),vi.end(),vi.begin(), [] (int i) {if(i<0) return -i; else return i;});

2.3.1 定义返回类型(尾置返回)

  当我们需要为一个lambda表达式定义返回类型时,必须使用尾置返回类型。尾置返回类型,顾名思义,就是将返回类型放置在后面。跟在参数列表之后,以"->"加上返回类型来表示。

// 正确:指定了返回类型为int
transform(vi.begin(),vi.end(),vi.begin(), [] (int i) -> int  {if(i<0) return -i; else return i;});

2.4 函数体

  函数体,即是我们需要lambda表达式执行的代码块。也就是lambda表达式"{ }"中的内容。

2.4.1 函数体中可以使用的变量

  在lambda函数体中,除了可以使用我们之前说过的捕获列表中的变量,还可以直接使用static变量它所在函数之外声明的名字。

int out_sz = 6;
auto A()
{
    static int static_sz = 6;
    vector<string> words = { "apple","banana","orange" };

    // 正确:Lambda可以直接使用它所在函数之外声明的变量
    auto we = find_if(words.begin(), words.end(), [](const string &a) { return a.size() >= out_sz; });

    // 正确:Lambda可以直接使用局部static变量
    auto we1 = find_if(words.begin(), words.end(), [](const string &a) { return a.size() >= static_sz; });

    // 错误:无法再lambda中捕获带有静态储存持续时间的变量
    auto we2 = find_if(words.begin(), words.end(), [static_sz](const string &a) { return a.size() >= static_sz; });
    auto we3 = find_if(words.begin(), words.end(), [out_sz](const string &a) { return a.size() >= out_sz; });

    return we;
}

2.4.1 函数体中可以使用的函数

  以下例子在函数体中使用了名字cout,cout不是定义在函数B中的局部名字,而是定义在头文件<iostream>中。因此,只要在B出现的作用域中包含了头文件<iostream>,我们的lambda就可以使用cout。

void B()
{
    vector<string> words = { "apple","banana","orange" };

    // 打印单词,每个单词后面接一个空格
    for_each(wc, words.end(), [] (const string &s) { cout << s << " "; });
    cout << endl;
}

2.4 可变lambda

  上面我们已经知道了值捕获时,捕获变量都为拷贝,无法修改原变量的值。如果我们希望能改变一个被捕获的变量的值,可以在参数列表后加上关键字mutable

void fcn3()
{
    size_t v1 = 42;
    // f可以改变它所捕获的变量的值;
    auto f = [v1] () mutable {return ++v1;};
     v1 = 0;
    auto j = f(); // j为43
}

这里要注意,不要跟引用捕获混淆哟!将上述fcn3修改为引用捕获,感受一下不同之处。

void fcn3()
{
    size_t v1 = 42;
    // f可以改变它所捕获的变量的值;
    auto f = [&v1] () {return ++v1;};
     v1 = 0;
    auto j = f(); // j为0
}

无论是通过值捕获去修改,还是通过引用捕获去修改,都要必须要捕获变量是可修改左值。如果上述fcn3中的v1变量是const,则不允许修改。

3 什么时候用lambda表达式?

   对于那种只在一两个地方使用的简单操作,lambda表达式是最有用的。如果我们需要在很多地方使用相同的操作,通常应该定义一个函数,而不是多次编写相同的lambda表达式。类似的,如果一个操作需要很多语句才能完成,通常使用函数更好。

  如果lambda表达式为空,我们通常可以用函数来替代它。如修改以下lambda示例:

// 按长度排序,长度相同的单词维持字典序
stable_sort (words.begin(), words.end(), [](const string &a, const string &b) { return a.size() <  b.size(); }); 

 改写为函数形式:

// 比较函数,用来按长度排序单词
bool isShorter(const string &s1, const string &s2) 
{
    return s1.size() < s2.size(); 
}

// 按长度由短至长排序words
sort(words begin(), words.end(), isShorter); 

  但是,对于捕获局部变量的lambda,用函数来替换它就不是那么容易了。例如以下示例:

// 获取一个迭代器,指向第一个满足size()>=6的元素 
auto A()
{
    int sz = 6;
    vector<string> words = {"apple","banana","orange"};

    // sz已捕获
    auto we = find_if(words.begin(), words.end(), [sz] (const string &a) { return a.size()>= sz; }); 
return we;
}

我们可以很容易将find_if函数中的lambda表达式,改写为一个完成同样工作的函数:

bool check_size(const string &s, string::size_type sz)
{
    return s.size()>= sz;
}

但是,我们不能用这个函数作为find_if的第三个参数。因为find_if函数的第三参数仅接受一元谓词,因此传递给find_if的可调用对象仅接受单一参数。为了用check_size 来代替此lambda,必须解决如何传递参数sz的问题

posted on 2021-05-07 17:19  天官赐福  阅读(593)  评论(0)    收藏  举报