10-3 定制操作lambda

10.3.1 向算法传递函数

作为一个例子,假定希望在调用elimDups(参见10.2.3节)后打印vector的内容。

此外还假定希望单词按其长度排序,大小相同的再按字典序排列。

为了按长度重排vector,我们将使用sort 的第二个版本,此版本是重载过的,它接受第三个参数,此参数是一个谓词(predicate)

谓词#

  • 谓词接受一个参数【一元谓词】或两个参数【二元谓词】,在所有的可能值上定义了一个一致的序
  • sort函数会用谓词函数代替“<”来对元素排序
//比较函数,按长度排序
bool isShorter(const string &s1, const string &s2){
    return s1.size() < s2.size();
}
//由短到长排序words
sort(words.begin(), words.end(), isShorter);

排序算法#

在我们将 words按大小重排的同时,还希望具有相同长度的元素按字典序排列。

为了保持相同长度的单词按字典序排列,可以使用stable_sort算法。这种稳定排序算法维持相等元素的原有顺序。

elimDups(words); //将words按字典序重排,消除重复单词
//按长度重排,长度相同的单词维持字典序
stable_sort(words.begin(), words.end(), isShorter);
//源程序
#include<iostream>
#include<vector>
#include<algorithm>
#include<iterator>
using namespace std;
void elimDups(vector<string> &words){
    //按字典排序words,以便查找重复单词
    sort(words.begin(), words.end());
    //unique重排输入范围,使得每个单词只出现一次
    //排列在范围前部,返回指向不重复区域之后一个位置的迭代器
    auto end_unique = unique(words.begin(), words.end());
    //使用earase删除重复单词
    words.erase(end_unique, words.end());
}
bool isShorter(const string &s1, const string &s2){
    return s1.size() < s2.size();
}
int main(){
    vector<string> words = {"slow","the","fox","jumps","over","quick",
            "red","the","turtle"};
    elimDups(words);
    stable_sort(words.begin(), words.end(), isShorter);
    for(string s : words)
        cout<<s<<" ";
    cout<<endl;
    return 0;
}
//输出:fox red the over slow jumps quick turtle

10.3.2 lambda表达式

引入#

求words中大于一个给定长度的单词有多少个,并打印满足条件的单词

将次函数命名为“biggies”,框架如下:

image-20220223152344583

现在的新问题是在vector中寻找第一个大于等于给定长度的元素

我们可以使用标准库find_if 算法来查找第一个具有特定大小的元素。

类似find(参见10.1节,第336页), find_if 算法接受一对迭代器,表示一个范围

但与 find不同的是,find_if 的第三个参数是一个一元谓词

find_if算法对输入序列中的每个元素调用给定的这个谓词。它返回第一个使谓词返回非О值的元素,如果不存在这样的元素,则返回尾迭代器。

编写一个函数,令其接受一个string 和一个长度,并返回一个bool值表示该string的长度是否大于给定长度,是一件很容易的事情。

但是,find_if接受一元谓词——我们传递给find_if 的任何函数都必须严格接受一个参数,以便能用来自输入序列的一个元素调用它。但我们又不得不给它传入第二个参数来表示长度。

为了解决此问题,需要使用另外一些语言特性。

介绍lambda#

lambda表达式是一个可调用对象

可调用对象(callable object):

对于任何一个对象或者表达式,只要可以对它使用调用运算符“()”,我们就称该对象或表达式是“可调用的”

常见的可调用对象

  • 函数和函数指针
  • 重载了函数调用运算符的类(14.8节介绍)
  • lambda表达式

lambda表达式可以理解为一个未命名的内联函数,可以定义在函数中

构成:

image-20220223153241082

  • capture list(捕获列表)是一个lambda所在函数中定义的局部变量的列表(通常为空);

  • return type、parameter list和 function body与任何普通函数一样,分别表示返回类型、参数列表和函数体。

  • 但是,与普通函数不同,lambda必须使用尾置返回(参见6.3.3节,第206页)来指定返回类型

可以忽略参数列表和返回类型,但必须包含捕获列表和函数体

//定义了一个lambda:不接受任何参数,返回42
auto f = []{return 42;};
//调用方式和函数相同,用调用运算符
cout<<f()<<endl; //打印42

当函数体只有一个return语句时,lambda表达式可以推断出返回类型

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

向lambda传递参数#

lambda不接受默认实参

传递参数举例:

[](const string &a, const string &b) 
	{return a.size() < b.size();}
  • “[]”:不需要所在函数中的任何局部变量
  • “(…)”:参数
  • 只有return语句,忽略返回类型
  • “{…}”:函数体
//按长度排序,长度相同维持字典序
stable_sort(words.begin(), words.end(),
           [](const string &a, const string &b)
            {return a.size()<b.size();});

使用捕获列表#

lambda表达式可以通过捕获列表,使用函数中定义的局部变量

biggies函数中有一个局部变量sz,可以定义如下lambda表达式来把参数的大小与sz比较

[sz](const string &a) {return a.sise() >= sz;}

一个 lambda只有在其捕获列表中捕获一个它所在函数中的局部变量,才能在lambda的函数体中使用该变量。【有点绕,具体看例子】

调用find_if#

现在可以写出find_if【在biggies函数内使用,biggies内定义了sz】了

//获取迭代器,指向第一个size>=sz的元素
auto wc = find_if(words.begin(), words.end(),
                 [sz](const string &a)
                  {return a.size() >= sz;});

使用for_each#

for_each(迭代器范围, 可调用对象):对迭代器范围内的每个元素调用传入的可调用对象

//打印长度>=给定值的单词,每个单词后面接一个空格
for_each(wc, wc.words.end(),
        [](string &s){cout<<s<<" ";});

完整的biggies#

void biggies(vector<string> &words, int sz){
    elimDups(words); //words按字典排序,删除重复单词
    //按长度排序,长度相同的维持字典序
    stable_sort(words.begin(), words.end(),
               	[](const string &a, const string &b)
                  {return a.size()<b.size();});
    //获取迭代器,指向第一个满足size()>=sz的元素
    auto wc = find_if(words.begin(), words.end(),
                     [sz](const string &s)
                      {return s.size()>=sz;});
    //计算满足size>=sz的元素个数
    auto count = words.end() - wc;
    cout<<"count = "<<count<<endl;
    //打印长度大于给定值的单词
    for_each(wc, words.end(),
            [](string &s)
             {cout<<s<<" ";});
    cout<<endl;
}

10.3.3 lambda的捕获和返回

当定义一个lambda时,编译器生成一个与lambda对应的新的(未命名的)类类型。我们将在14.8.1节(第507页)介绍这种类是如何生成的。

目前,可以这样理解,当向一个函数传递一个lambda 时,同时定义了一个新类型和该类型的一个对象:传递的参数就是此编译器生成的类类型的未命名对象。

类似的,当使用auto定义一个用lambda初始化的变量时,定义了一个从lambda生成的类型的对象。

默认情况下,从 lambda生成的类都包含一个对应该lambda所捕获的变量的数据成员。类似任何普通类的数据成员,lambda的数据成员也在lambda对象创建时被初始化。

值捕获#

类似于参数传递,捕获的方式可以是值捕获也可以是引用捕获

采用值捕获的前提是可以拷贝

与函数参数传递不同,lambda表达式捕获的变量的值是在创建时拷贝,而不是调用时拷贝

void fcn1(){
    int v1 = 42;
    auto f = [v1]{return v1;};
    v1 = 0;
    cout<<f()<<endl;
    //输出为42
}

引用捕获#

void fcn2(){
    int v1 = 42;
    auto f = [&v1]{return v1;};
    v1 = 0;
    cout<<f()<<endl;
    //输出为0
}

引用捕获是必要的,毕竟有些对象如ostream对象,是无法拷贝的,只能采用引用捕获

采用引用方式捕获一个变量,就必须确保被引用的对象在lambda执行的时候是存在的

lambda捕获的都是局部变量,这些变量在函数结束后就不复存在了。如果 lambda可能在函数结束后执行,捕获的引用指向的局部变量已经消失。

我们也可以从一个函数返回 lambda。函数可以直接返回一个可调用对象,或者返回一个类对象,该类含有可调用对象的数据成员。如果函数返回一个lambda,则与函数不能返回一个局部变量的引用类似,此lambda也不能包含引用捕获,因为捕获的对象是局部对象,在函数结束时已经消失。

建议:尽量保持lambda的变量捕获简单化
一个 lambda捕获从 lambda被创建(即,定义 lambda的代码执行时)到lambda自身执行(可能有多次执行)这段时间内保存的相关信息。确保 lambda每次执行的时候这些信息都有预期的意义,是程序员的责任。

  • 捕获一个普通变量,如int.string或其他非指针类型,通常可以采用简单的值捕获方式。在此情况下,只需关注变量在捕获时是否有我们所需的值就可以了。

  • 如果我们捕获一个指针或迭代器,或采用引用捕获方式,就必须确保在 lambda执行时,绑定到迭代器、指针或引用的对象仍然存在。而且,需要保证对象具有预期的值在 lambda从创建到它执行的这段时间内,可能有代码改变绑定的对象的值。也就是说,在指针(或引用)被捕获的时刻,绑定的对象的值是我们所期望的,但在 lambda执行时,该对象的值可能已经完全不同了。

一般来说,我们应该尽量减少捕获的数据量,来避免潜在的捕获导致的问题。而且.如果可能的话,应该避免捕获指针或引用。

隐式捕获#

除了显式地指定捕获的变量外,可以让编译器根据代码自动推断我们要使用哪些变量,此时应在捕获列表写一个&=

  • &:引用捕获

  • =:值捕获

我们可以重写find_if

//sz为隐式捕获,值捕获
wc = find_if(words.begin(), words.end(),
            [=](const string &s)
             {return s.size()>=sz;});

可以混合使用显示捕获和隐式捕获

void print(vector<string> &words, ostream &os, 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;});
}
  1. 混合使用显示捕获和隐式捕获时,第一个参数必须是&=
  2. 显示捕获和隐式捕获的变量必须采取不同的捕获方式

image-20220223195624331

可变lambda#

默认情况下

  • 引用捕获时,可以改变捕获变量的值

  • 值捕获时,无法改变捕获变量的值,但可以通过添加mutable关键字实现这点

void fcn3(){
    int v1 = 42;
    //此时参数列表不可省略
    auto f = [v1] () mutable {return ++v1;};
    vl = 0;
    cout<<f()<<endl;
    //输出为43
}

指定lambda的返回类型#

  • 当lambda函数体中只包含return语句,所以lambda可以自动推断返回类型,返回类型可以省略

  • 当lambda中包含其他语句(循环,判断…),就必须指定返回类型,否则默认返回void

下面给出了一个简单的例子

我们可以使用标准库 transform算法和一个lambda来将一个序列中的每个负数替换为其绝对值:

transform(vi.begin(), vi.end(), vi.begin(), 
            [](int i){return i<0 ? -i : i;});

函数transform接受三个迭代器一个可调用对象

前两个迭代器表示输入序列,第三个迭代器表示目的位置算法对输入序列中每个元素调用可调用对象,并将结果写到目的位置。

如本例所示,目的位置迭代器与表示输入序列开始位置的迭代器可以是相同的。当输入迭代器和目的迭代器相同时,transform将输入序列中每个元素替换为可调用对象操作该元素得到的结果。

本例子中,lambda中只有return语句,所以无需指定返回类型,如果改写为:

transform(vi.begin(), vi.end(), vi.begin(), 
            [](int i){if(i<0) return -i; else return i;});

会产生编译错误,应该指定返回类型,写为:

transform(vi.begin(), vi.end(), vi.begin(), 
            [](int i)->int{if(i<0) return -i; else return i;});

ps.不知道为什么我没有指定返回类型在vscode上也通过编译了,不过vscode编译检查并不严格,为了保证程序的可移植性,最好加上返回类型

int main(){
    fcn3();
    vector<int> vi = {1,-2,4,-3};
    transform(vi.begin(), vi.end(), vi.begin(), 
            [](int i)->int{if(i<0) return -i; else return i;});
    for_each(vi.begin(), vi.end(), [](int i){cout<<i<<" ";});
    return 0;
    //输出1,2,4,3
}

算法的if版本#

在10.2.2 已经讲过,算法的拷贝版本xxx_copy(指定容器的迭代器,...)可以将函数的运行结果拷贝到指定的容器中

而算法的if版本xxx_if(...,谓词)则是对迭代器范围内的元素调用可调用对象

  • find_if(iter1, iter2, 一元谓词):返回第一个使得谓词为真的元素的迭代器

  • cout_if(iter1, iter2, 谓词):返回使得谓词为真的元素的个数

    int main(){
        vector<int> vi = {1,-2,4,-3,5};
        cout<<count_if(vi.begin(), vi.end(), [](int i){return i>0;})<<endl;
        return 0;
        //输出为3
    }
    
  • for_each(iter1, iter2, 可调用对象):对范围内的每个元素调用可调用对象

    //遍历的新写法
    for_each(vi.begin(), vi.end(), [](int i){cout<<i<<" ";});
    
  • transform(iter1, iter2, iter3, 可调用对象):见上文

posted @   咪啪魔女  阅读(44)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Ollama——大语言模型本地部署的极速利器
· 使用C#创建一个MCP客户端
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· Windows编程----内核对象竟然如此简单?
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
more_horiz
keyboard_arrow_up light_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示