很多算法都会比较输入序列中的元素。默认情况下,这类算法使用元素类型的<或==运算符完成比较。标准库还为这些算法定义了额外的版本,允许我们提供自己定义的操作来代替默认运算符。

例如,sort算法默认使用元素类型的<运算符。但可能我们希望的排序顺序与<所定义的顺序不同,或是我们的序列可能保存的是未定义<运算符的元素类型(如Sales_data).在这两种情况下,都需要重载sort的默认行为。

1 向算法传递函数

作为一个例子,假定希望在调用elimDups后打印vector的内容。此外还假定希望单词按其长度排序,大小相同的再按字典序排列。为了按长度排序vector,我们将使用sort的第二个版本,此版本是重载过的,它接受三个参数,此参数是一个谓词

谓词

谓词是一个可调用的表达式,其返回结果是一个能用作条件的值。标准库算法所使用的谓词分为两类:一元谓词(意味着它们只接受单一参数)和二元谓词(意味着它们接受两个参数)。接受谓词参数的算法对输入序列中的元素调用谓词。因此,元素类型必须能转换为谓词的参数类型

接受一个二元谓词参数的sort版本用这个谓词代替<来比较元素。

//比较函数,用来比较长度排序单词

bool isShorter(const string &s1,const string &s2)

{

  return s1.size()<s2.size();

}

//按长度由短至长排序words

sort(words.begin(),words.end(),isShorter);

排序算法

在我们将words按大小重排的同时,还希望具有相同长度的元素按字典序排列。为了保持相同长度的单词按字典序排列,可以使用stable_sort算法。这种稳定排序算法维持相等元素的原有顺序。

通常情况下,我们不关心有序序列中相等元素的相对顺序,它们毕竟是相等的。通过调用stable_sort,可以保持等长元素间的字典序:

elimDups(words);//将words按字典序重排,并消除重复单词

//按长度重新排序,长度相同的单词维持字典序

stable_sort(words.begin(),words.end(),isShorter);

for(const auto &s:words)

  cout<<s<<" ";

cout<<endl;

2 lambda表达式

根据算法接受一元谓词还是二元谓词,我们传递给算法的谓词必须严格接受一个或两个参数。但是,有时我们希望进行的操作需要更多参数,超出了算法对谓词的限制。例如,我们求大于等于一个给定长度的单词有多少。并且只打印打印等于给定长度的单词。

我们将此函数命名为biggies,其框架如下所示:

void biggies(vector<string> &words,vector<string>::size_type sz)
{
elimDups(words);
stable_sort(words.begin(),words.end(),isShorter);
//获取一个迭代器,指向第一个满足size()>=sz 的元素
//计算满足size>=sz的元素的数目
//打印长度大于等于给定值的单词,每个单词后面跟一个空格
}

我们的新问题是在vector中寻找第一个大于等于给定长度的元素。一旦找到了这个元素,根据其位置,就可以计算出有多少元素的长度大于等于给定值。

我们可以使用标准库find_if算法来查找第一个具有特定大小的元素。类似find,find_if算法接受一对迭代器,表示一个范围。但与find不同的是,find_if的第三个参数是一个谓词。find_if算法对输入序列中的每个元素调用给定的这个谓词。它返回第一个使谓词返回非0值的元素,如果不存在这样的元素,则返回尾迭代器。

编写一个函数,令其接受一个string和一个长度,并返回一个bool值表示该string的长度是否大于给定长度,是一件很容易的事情。但是,find_if接受一元谓词——我们传递给find_if的任何函数都必须接受一个参数,以便能用来自输入序列的一个元素调用它。没有任何办法能传递给它第二个参数来表示长度。为了解决此问题,需要使用另外一些语言的特性。

介绍lambda

我们可以向一个算法传递任何类别的可调用对象。对于一个对象或一个表达式,如果可以对其使用调用运算符,则称它为可调用的。即,如果e是一个可调用的表达式,则我们可以编写代码e(args),其中args是一个逗号分隔的一个或多个参数的列表。

到目前为止,我们使用过的仅有的两种可调用对象是函数和函数指针。还有其他两种可调用对象:重载了函数调用运算符的类,以及lambda表达式

一个lambda表达式表示一个可调用的代码单元。我们可以将其理解为一个未命名的内联函数。与任何函数类似,一个lambda具有一个返回返回类型、一个参数列表和一个函数体。但与函数不同,lambda可能定义在函数内部。一个lambda表达式具有如下形式

  [capture list] (parameter list) ->return type { function body }

其中,capture list(捕获列表)是一个lambda所在函数中定义的局部变量列表(通常为空);return type、parameter list和function body与任何普通函数一样,分别表示返回类型、参数列表和函数体。但是,与普通函数不同,lambda必须使用尾置返回来指定返回类型。

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

auto f=[] {return 42;};

此例中,我们定义了一个可调用对象,它不接受参数,返回42.

lambda的调用方式与普通函数的调用方式相同,都是使用调用运算符:

cout<<f()<<endl; //打印42

在lambda中忽略括号和参数列表等价于指定一个空参数列表。在此例中,当调用f时,函数参数列表是空的。如果忽略返回类型,则返回类型从返回的表达式的类型推断而来。否则,返回类型为void。

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

向lambda传递参数

与普通函数调用类似,调用一个lambda时给定的实参类型被用来初始化lambda的形参。通常,实参和形参的类型必须匹配,但与普通函数不同,lambda不能有默认参数。因此,一个lambda调用的实参数目永远与形参数目相等。一旦形参初始化完毕,就可以执行函数体了。

作为一个带参数的lambda的例子,我们可以编写一个与isShorter函数完成相同功能的lambda:

[] (const string &s1,const string &s2)

{ return s1.size()<s2.size();}

空捕获列表表明此lambda不使用它所在函数中的任何局部变量。lambda的参数与isShorter的参数类似,是const string 的引用。lambda的函数体也与isShorter类型,比较两个参数的size(),并根据两者的相对大小返回一个布尔值。

如下所示,可以使用此lambda来调用stable_sort;

stable_sort(words.begin(),words.end(),[](const string &s1,const string &s2)

                  {return s1.size()<s2.size();});

当stable_sort需要比较两个元素时,它就会调用给定的这个lambda表达式。

使用捕获列表

编写一个可以传递给find_if的可调用表达式。我们希望这个表达式能将输入序列中每个string的长度与biggies函数中的sz参数的值进行比较。

虽然一个lambda可以出现在一个函数中,使用其局部变量,但它只能使用那些明确指明的变量。一个lambda通过将局部变量包含在其捕获列表中指出将会使用这些变量。捕获列表指引lambda在其内部包含访问局部变量所需的信息。

在本例中,我们的lambda会捕获sz,并只有单一的string参数。其函数体会将string的大小与捕获的sz的值进行比较:

[sz] (const string &s)

  {return s.size()>=sz;};

lambda以一对[]开始,我们可以在其中提供一个以逗号分隔的名字列表,这些名字都是它所在函数中定义的。

由于此lambda捕获sz,因此lambda的函数体可以使用sz。lambda不捕获words,因此不能访问此变量。如果我们给lambda提供了一个空捕获列表,则代码会编译错误:

//错误:sz未捕获

[] (const string &s);

  { return s.size()>=sz;};

注意:一个lambda只有在其捕获列表中捕获一个所在函数中的局部变量,才能在函数体中使用该变量。

调用find_if

使用此lambda,我们就可以查找第一个长度大于等于sz的元素:

//获取一个迭代器,指向第一个满足size()>=sz的元素

auto wc=find_if(words.begin(),words.end(),

       [sz] (const string &s)

        {return s.size()>=sz;});

这里对find_if的调用返回一个迭代器,指向第一个长度不小于给定参数sz的元素。如果这样的元素不存在,则返回words.end()的一个拷贝。

我们可以使用find_if返回的迭代器来计算从它开始到words的末尾一共有多少个元素:

//计算满足size>=sz的元素的数目

auto count=words.end() -wc;

for_each算法

打印words中长度大于等于sz 的元素。为了达到这一目的,我们可以使用for_each算法,此算法接受一个可调用对象,并对输入序列中每个元素调用此对象:

//打印长度大于等于给定值的单词,每个单词后面接一个空格

for_each(wc,words.end(),

  [](const string &s) {cout<<s<<" ";});

此lambda中的捕获列表为空,但其函数体中还是使用了两个名字:s和cout,前者是它自己的参数。

捕获列表为空,是因为我们只对lambda所在函数中定义的(非static)变量使用捕获列表。一个lambda可以直接使用定义在当前函数之外的名字。在本例中,cout不是定义在biggies中的局部名字,而是定义在头文件iostream中。因此,只要在biggies出现的作用域中包含了头文件iostream,我们的lambda就可以使用cout。

注意:捕获列表只用于局部非static变量,lambda可以直接使用局部static变量和在它所在函数之外声明的名字。

3 lambda捕获和返回

当定义一个lambda时,编译器生成一个与lambda对应的新的(未命名的)类类型。当向一个函数传递一个lambda时,同时定义了一个新类型和该类型的一个对象;传递的参数就是此编译器生成的类类型的未命名对象。类似的,当使用auto定义一个用lambda初始化的变量时,定义了一个从lambda生成的类型的对象。

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

值捕获

类似参数传递,变量的捕获方式也可以是值或引用。到目前为止,我们的lambda采用值捕获的方式。与传值参数类似,采用值捕获的前提是变量可以拷贝与参数不同,被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝

void fcn1()

{

  size_t v1=42;  //局部变量

  //将v1拷贝到名为f的可调用对象

  auto f=[v1] {return v1;};

  v1=0;

  auto j=f(); //j为42;f保存了我们创建它时v1的拷贝

}

由于被捕获变量的值是在lambda创建时拷贝,因此随后对其修改不会影响到lambda内对应的值。

引用捕获

我们定义lambda时可以采用引用方式捕获变量。例如:

void fcn2()

{

  size_t v1=42;

  //对象f2包含v1的引用

  auto f2=[&v1] { return v1;};

  v1=0;

  auto j=f2();  //j为0;f2保存v1的引用,而非拷贝

}

v1之前的&指出v1应该以引用方式捕获。一个以引用方式捕获的变量与其他任何类型的引用的行为类似。当我们在lambda函数体内使用此变量时,实际上使用的是引用所绑定的对象。在本例中,当lambda返回v1时,它返回的是v1指向的对象的值。

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

引用捕获有时是必要的。例如,我们可能希望biggies函数接受一个ostream的引用,用来输出数据,并接受一个字符作为分隔符:

void biggies(vector<string> &words,vector<string>::size_type sz,ostream &os=cout,char c=' ')

{

  //下面打印count的语句改为打印到os

  for_each(words.begin(),words.end(),[&os,c](const string &s) {os<<s<<c;});

}

我们不能拷贝ostream对象,因此捕获os的唯一方式就是捕获其引用(或指向os的指针)。

当我们向一个函数传递一个lambda时,就像本例中调用for_each那样,lambda会立即执行。在此情况下,以引用方式捕获os没有问题,因为当for_each执行时,biggies中的变量是存在的。

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

注意:当以引用方式捕获一个变量时,必须保证在lambda执行时变量是存在的。

隐式捕获

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

//sz为隐式捕获,值捕获方式

wc=find_if(words.begin(),words.end(),

  [=] (const string &s)

    { return s.size()>=sz; });

如果我们希望对一部分变量采用值捕获,对其它变量采用引用捕获,可以混合使用隐式捕获和显式捕获:

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;});

}

当我们混合使用隐式捕获和显式捕获时,捕获列表中的第一个元素必须是一个&或=,此符号指定了默认捕获方式为引用或值。

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

lambda捕获列表

[]        空捕获列表。lambda不能使用所在函数中的变量。一个lambda只有捕获变量后才能使用它们

[names]       names是一个逗号分隔的名字列表,这些名字都是lambda所在函数的局部变量。默认情况下,捕获列表中的变量都被拷贝。名字前如果使用了&,则采用引用方式捕获

[&]        隐式捕获列表,采用引用捕获方式。lambda体中所使用的来自所在函数的实体都采用引用方式使用

[=]           隐式捕获列表,采用值捕获方式。lambda体将拷贝所使用的来自所在函数的实体的值

[&,identifier_list] identifier_list是一个逗号分隔的列表,包含0个或多个来自所在函数的变量。这些变量采用值捕获方式,而任何隐式捕获的变量都采用引用方式捕获。identifier_list中的名字前面不能加&

[=,identifier_list] identifier_list中的变量都采用引用方式捕获,而任何隐式捕获的变量都采用值方式捕获。identifier_list中的名字不能包括this,且这些名字之前必须使用&

可变lambda

默认情况下,对于一个值被拷贝的变量,lambda不会改变其值。如果我们希望能改变一个捕获的变量的值,就必须在参数列表首加上关键字mutable。因此,可变lambda能省略参数列表:

void fcn3()

{

  size_t v1=42; //局部变量

  //f可以改变它所捕获的变量的值

  auto f=[v1] () mutable {return ++v1;};

  v1=0;

  auto j=f(); //j为43

}

一个引用捕获的变量是否(如往常一样)可以修改依赖于此引用指向的是一个const类型还是一个非const类型:

void fcn4()

{

  size_t v1=42;  //局部变量

  //v1是一个非const变量的引用

  //可以通过f2中的引用来改变它

  auto f2=[&v1] {return ++v1;};

  v1=0;

  auto j=f2(); //j为1

}

指定lambda返回类型

到目前为止,我们所编写的lambda都只包含单一的return语句。因此,我们还未遇到必须指定返回类型的情况。默认情况下,如果一个lambda体包含return之外的任何语句,则编译器假定此lambda返回void。与其它返回void的函数类型类似,被推断返回void的lambda不能返回值。

下面给出了一个简单的例子,我们可以使用标准库transform算法和一个lambda来将一个序列中的每个负数替换为其绝对值:

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

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

在本例中,我们传递给transform一个lambda,它返回其参数的绝对值。lambda体是单一的return语句,返回一个条件表达式的结果。我们无须指定返回类型,因为可以根据条件运算符的类型推断出来。

但是,如果我们将程序改写为看起来是等价的if语句,就会产生编译错误:

//错误:不能推断lambda的返回类型

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

编译器推断这个版本的lambda返回类型是void,但它返回了一个int值

当我们需要为一个lambda定义返回类型时,必须使用尾置返回类型:

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

在此例中,传递给transform的第四个参数是一个lambda,它的捕获列表是空的,接受单一int参数,返回一个int值。它的函数体是一个返回其参数的绝对值的if语句。

定制操作(传递函数或lambda表达式)的更多相关文章

  1. Util应用程序框架公共操作类(九):Lambda表达式扩展

    上一篇对Lambda表达式公共操作类进行了一些增强,本篇使用扩展方法对Lambda表达式进行扩展. 修改Util项目的Extensions.Expression.cs文件,代码如下. using Sy ...

  2. Util应用程序框架公共操作类(八):Lambda表达式公共操作类(二)

    前面介绍了查询的基础扩展,下面准备给大家介绍一些有用的查询封装手法,比如对日期范围查询,数值范围查询的封装等,为了支持这些功能,需要增强公共操作类. Lambda表达式公共操作类,我在前面已经简单介绍 ...

  3. Util应用程序框架公共操作类(七):Lambda表达式公共操作类

    前一篇扩展了两个常用验证方法,本文将封装两个Lambda表达式操作,用来为下一篇的查询扩展服务. Lambda表达式是一种简洁的匿名函数语法,可以用它将方法作为委托参数传递.在Linq中,大量使用La ...

  4. List的定制排序 包括使用lambda表达式来实现的方法

    1.先实现Comparator的接口 重写compare方法 根据比较大小来返回数值: 比如:(Integer o1  -   Integer o2); return 1 表示o1>o2; re ...

  5. Util应用程序框架公共操作类(十二):Lambda表达式公共操作类(三)

    今天在开发一个简单查询时,发现我的Lambda操作类的GetValue方法无法正确获取枚举类型值,以至查询结果错误. 我增加了几个单元测试来捕获错误,代码如下. /// <summary> ...

  6. Java Lambda表达式初探

    Java Lambda表达式初探 前言 本文受启发于Trisha Gee在JavaOne 2016的主题演讲Refactoring to Java 8. Java 8已经发行两年多,但很多人仍然在使用 ...

  7. Linq专题之Lambda表达式

    这一节我们讲的Lambda表达式跟匿名函数有关.Lambda表达式就是一个匿名函数,它可以包含表达式和语句,并且可以创建委托和表达式树. Lambda表达式的组成: 输入参数.Lambda运算符(=& ...

  8. Java8中Lambda表达式的10个例子

    Java8中Lambda表达式的10个例子 例1 用Lambda表达式实现Runnable接口 //Before Java 8: new Thread(new Runnable() { @Overri ...

  9. C++11 lambda表达式学习

    lambda表达式是函数式编程的基础.咱对于函数式编程也没有足够的理解,因此这里不敢胡言乱语,有兴趣的可以自己查找相关资料看下.这里只是介绍C++11中的lambda表达式自己的认识.这里有参考文档h ...

随机推荐

  1. 3.5MM/2.5MM耳机插头定义

    自2009年国内手机耳机接口统一以来,现在国内销售耳机基本就3.5MM和2.5MM两种,当然也有很少BT厂家,就是不用这两种,比如使用MiniUSB等接口作为耳机接口.3.5mm耳机插头按照结构划分, ...

  2. Dagger 2: Step To Step

    文/iamwent(简书作者)原文链接:http://www.jianshu.com/p/7505d92d7748著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”. 假设你已经了解 依赖注 ...

  3. 引用计数(retainCount)

    ClassA.h: #import <Foundation/Foundation.h> @interface ClassA:NSObject { NSString *name; } -(v ...

  4. plsql exist和in 的区别

    <![endif]--> <![endif]--> 发现公司同事很喜欢用exists 和in 做子查询关联,我觉得很有必要研究下 两者的区别,供参考和备忘 /* (这段信息来自 ...

  5. app与server联系

    --- /frameworks/base/services/java/com/android/server目录下SystemServer.java 在ServerThread的run函数中增加加载Rf ...

  6. Good vs Evil

    Good vs Evil Description Middle Earth is about to go to war. The forces of good will have many battl ...

  7. Sublime Text修改显示图标

    选择喜欢的图片 首先你需要选择一个中意的图片做为新的图标,格式可以是png,jpg,gif的 转为ico格式 我们需要ico格式的图片,所以需要将上述的图片转换一下格式.同样,转ico格式的软件很多, ...

  8. xfce terminal tab被解释为super+tab的bug

    https://bugzilla.xfce.org/show_bug.cgi?id=10760 解决方法 Every few weeks the tab key stops working in th ...

  9. linux中cron用法

    http://blog.chinaunix.net/uid-677314-id-83387.html

  10. Linux/U-Boot Git Repo

    Linux: git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git   U-Boot: git://git.denx.de/ ...