🥔定制操作
2022-5-25
| 2023-8-2
0  |  阅读时长 0 分钟
type
status
date
slug
summary
tags
category
icon
password
Property
 
 

 
大多数算法会比较容器中的元素。默认情况下算法使用的是 < 或 == 操作符。算法还定义了允许我们提供自己的操作来替换默认操作符的版本。
比如sort 算法使用的是 < 操作符。但序列可能不是按照<进行排序,或者有些类型没有<操作符,在这两种情况下都需要重载默认的 sort 行为。
 

向算法传递函数

对于字符串序列进行排序可以是先按照长度进行排序,对于长度一样的再按照字典顺序进行排序。为了这样做需要使用第二个版本的 sort,这个版本的 sort是重载过的,它有第三个参数称之为谓词(predicate)

谓词

谓词是一个可以被调用然后返回一个值的表达式,返回值可以作为条件使用。标准库使用的谓词可以是一元谓词(只有一个参数),也可以是二元谓词(两个参数)。接受谓词参数的算法对输入序列中的元素上调用这个谓词。因而,必须要可以将元素类型转为谓词的参数类型。
接受二元谓词参数的 sort 版本将给定谓词替换 < 操作用于比较元素。提供给 sort 算法的谓词必须满足一些限制,现在只需要知道这个操作必须在每一个元素之间提供稳定的顺序。单纯比较字符串的长度就是其中一个例子:
 

排序算法

当使用长度进行排序时,还希望保持相同长度的元素之间的字典排序。为了达到这样的效果,使用stable_sort 算法,稳定排序将维持相等元素的原有顺序。
通过调用 stable_sort 可以保持相同长度的字符串的字典顺序:
 
 

lambda 表达式

传递给算法的谓词必须有一个或两个参数,但是有时想传递多于算法的谓词需要的参数。
为了查找字符串序列中大于等于给定长度的字符串:
使用 find_if 标准库算法来查找一个元素大于等于给定长度的。find_if 接收一对迭代器表示输入范围,不同于 find 的是它接收的第三个参数是谓词。find_if 算法在每个元素上调用给定的谓词。它将返回第一个使得谓词的返回值为非 0 值的元素迭代器,或者当无法找到这样的元素时返回尾部迭代器。
由于find_if接收的是一元谓词,任何传递给find_if的函数必须只有一个参数。所以想要传递可变的size参数给谓词必须使用lambda表达式。
 

lambda

可以传递任何可调用对象给算法。如果可以给对象或表达式运用调用操作符,它就是可调用的(callable)。也就是说如果 e 是可调用的表达式,那么可以写作 e(args) 其中 args 是一个逗号分隔的零个或多个参数的列表。
之前使用过的可调用对象就是函数和函数指针。还有两种可调用对象:重载了调用操作符的类,以及lambda表达式。
 
lambda 表达式表示可调用的代码单元。可以被视作匿名内联函数。与任何函数一样,lambda 有返回类型,参数列表和函数体。与函数不同的是,lambda 可以被定义在函数中,其有如下形式:
  • [capture list] 捕获列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
  • (parameter list):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
  • mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略
  • >return type:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
  • {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
 
可以忽略参数列表和返回类型,但是必须总是包含捕获列表和函数体:
 
使用lambda的方式与任何函数调用都是一样的:
省略lambda中的括号和参数列表表示其参数列表为空。如果省略返回值类型,lambda 通过函数体中语句进行推断。
如果函数体中只有一个 return 语句,那么返回类型就是被返回的表达式的类型。如果包含了除了返回语句之外的任何语句,且未指定返回类型,则返回void
 
 

传递参数给lambda

与普通的函数调用一样,lambda调用中的实参也是用于初始化其形参的。实参和形参的类型必须匹配。与普通函数不同的是,lambda没有默认实参。因而,调用lambda给足实参。一旦形参被初始化,函数体就开始执行。
 
如下lambda需要传递参数:
空的捕获列表表示不使用任何外围函数的局部变量。lambda的参数与常规函数的参数一样是const string的引用。可以重写 stable_sort的调用从而使用 lambda
stable_sort需要比较元素值时,它将调用给定的lambda表达式。
 

使用捕获列表

上面使用的 find_if 中的 lambda 表达式中的捕获列表中的 sz。尽管lambda 被定义在函数中,只有被指定了想要使用的外围函数本地变量才能使用。lambda 通过捕获列表来指定想要使用的变量。捕获列表中包含了 lambda 用于访问外围函数本地变量的信息。
这样 find_if 使用的 lambda 表达式捕获了 sz,并且只有一个参数,函数体中将给定string的长度与捕获值sz进行比较:
[]中是逗号分隔的捕获列表,里边的名字是外围函数中定义的本地变量。由于这个lambda捕获了sz,函数体就可以使用sz,而没有捕获words 就不能访问此变量。
注:lambda只能使用出现在捕获列表中的外围函数本地变量。
 
 

for_each 算法

for_each 算法有一个可调用对象并在输入范围内的每个元素上调用此对象:
注:这里并没有捕获 cout,捕获列表只用于捕获外围函数中定义的非静态变量。lambda 可以自由使用定义在函数外的变量,此处cout 并不是一个本地变量;这个名字定义在 iostream 头文件中。只要包含了 iostream 头文件,这个 lambda就可以使用 cout
 
捕获列表只用于捕获外围函数中定义的非静态变量,lambda可以自由使用本地static 变量或者定义在函数体外的变量。
 
 

lambda 捕获和返回

当定义 lambda 时,编译器为此lambda产生一个匿名的新的类类型。当向一个函数传递一个lambda时,同时定义了一个新类型和该类型的一个对象:传递的参数就是此编译器生成的类类型的未命名对象。类似的,当使用auto定义一个用lambda初始化的变量时,定义了一个从lambda生成的类型对象。
 
默认情况下,从lambda中生成的类都包含一个对应该lambda 所捕获的变量的数据成员。类似任何普通类的数据成员,lambda的数据成员也在lambda对象创建时被初始化。
 
以下是所有的捕获方式:
notion image

值捕获

与参数传递一样,可以通过值或引用捕获变量。目前使用到的是值捕获。值捕获中的变量必须是可拷贝的。与参数不同的是,捕获变量是在 lambda 创建时拷贝的,而不是在调用时:
由于在创建 lambda 时已经拷贝了值,因而接下来对捕获变量的修改不会影响 lambda 中对应的值
 

引用捕获

可以定义以引用捕获变量的lambda
v1前的&表示 v1 是引用捕获的。被引用捕获的变量与其它引用是一样的。当在 lambda 函数体中使用引用捕获的变量时,使用的是这个引用绑定的对象。这个例子中返回的 v1 其实是其绑定的外围函数中的本地变量的值。
 
使用引用捕获的变量跟常规的使用引用变量一样,当lambda 被调用时需要确保引用捕获的变量确实存在。被 lambda 捕获的变量是本地变量,这些变量在函数完成时将消失。如果在函数结束后还可以调用 lambda ,那么将是严重的编程错误。
引用捕获有时是必须的,比如不可复制的对象:
可以从函数中返回 lambda ,函数可以直接返回可调用对象或者函数返回一个具有可调用对象数据成员类对象。如果函数返回 lambda ,那么与函数不能返回本地变量的引用一样,这个 lambda 一定不能包含引用捕获。
注:当进行引用捕获时,一定要保证当 lambda 执行时变量是存在的。
 
建议:保持 lambda 的捕获尽量的简单
lambda 捕获的信息在创建时保存,在执行时进行访问。保证这些信息的可用性是程序员自己的责任。按值的方式捕获 int ,string 和其它非指针变量是很简单的。这个时候,我们只需要关心在捕获时变量是否有我们需要的值。
如果捕获指针或迭代器或者按引用捕获,我们需要保证绑定到迭代器,指针和引用的对象在 lambda 执行时依然存在。甚至需要保证这个对象值是合理的。在 lambda 创建和执行之间的代码可能会改变按引用捕获的对象值。可能在创建时的值的确是我们想要的,但是当执行时其值已经变得很不一样了。
为了避免由捕获带来的问题尽可能缩小捕获的变量,并且尽可能避免捕获指针,迭代器和引用。
 

隐式捕获

相比于显式列举需要捕获的变量,可以让编译器对lambda 函数体的代码进行推断。为了引导编译去推断捕获列表,在捕获列表中使用 & 或 =,其中 & 告知编译器按引用捕获,= 告知编译按值捕获。如以下是重写的 find_if
 
如果想让其中的一些变量按值捕获,可以混用隐式和显式捕获:
 

可变Lambdas

默认情况下,lambda 不会改变按值捕获的变量值,如果想要改变捕获变量的值,必须在参数列表后加上关键词 mutable。有 mutable 关键词的 lambda 不能省略参数列表:
按引用捕获的对象是否可以改变仅仅依赖于引用的对象是 const 还是非 const 的:
 

指定 lambda 的返回类型

目前编写的 lambda 只有一个 return 语句。因而不需要指定返回类型。如果 lambda函数体中包含了任何不是 return 语句的话,那么 lambda 将被推断为返回 void。推断为返回 voidlambda 不会返回任何值:
transform 将输入序列中的每个元素通过调用 lambda 进行转换,并将结果存储到其目的迭代器所表示的序列中。目的序列和输入序列可以是同一个。由于这里的 lambda 中语句多于一条,必须指定其返回值。
 
 

参数绑定

lambda 表达式对于只用于一两个地方的简单操作非常有用。如果需要将相同的操作用于多个地方的话,应该定义函数而不是重复相同的 lambda 表达式。同样,如果操作比较复杂的话,最好是定义函数。
对于没有捕获列表的 lambda 表达式很容器用函数来替换。然而,想用函数来替换捕获了本地变量的 lambda 就有难度了:
想将 check_size 函数用于 find_if 肯定是不行的,find_if 要求函数只能接收一个参数。为了能够传递 check_size 必须要进行参数绑定。
 

标准库 bind 函数

使用定义了 functional 头文件中的 bind 函数。bind 可以被认为是通用的函数适配器(function adaptor)。它接收一个可调用对象并产生一个新的可调用对象,其中改变了原始函数的参数列表。bind的通用形式是:
其中 newCallable 是一个可调用对象,arg_list 是逗号分隔的参数并与给定的 callable 中的参数一一对应。也就是说,当调用 newCallable 时,newCallable将调用 callable,并传递参数列表 arg_list
参数列表 arg_list 可以包含形式为 _n 的名字,其中 n 是一个整数。这种形式的参数称之为 “占位符”(placeholder),表示 newCallable 的形参列表。意味着当调用 newCallable 并传递给其多个参数,这些参数会被填值到 _1 _2 _3 并以 arg_list 的顺序传递给 callable 可调用对象。传递给 newCallable 的参数个数与占位符的个数一样多。
 

将 sz 绑定到 check_size 上

以下是给 check_size 绑定一个固定的 sz 参数:
这个例子中 bind 只有一个占位符,意味着 check6 有一个参数。占位符出现在 arg_list 的第一个位置,意味着 check6 的参数对应于 check_size 的第一个参数。这个参数的类型是 const string&,意味着 check6 中的参数也是 const string&。那么当调用 check6 时就必须传递类型为 string 的参数,check6 会将其传递给 check_size 的第一个参数。
 
arg_list 中的第二个参数是值 6,这个值被绑定到 check_size 的第二个参数上,无论任何时候调用 check6,它都会将 6 传递给 check_sz 的第二个参数:
使用 bind 可以替换原来的 lambda 版本的 find_if
bind 的调用生成了一个新的可调用对象,并将 check_size 的第二个参数绑定到值 sz 上。当 find_if 调用此对象时将会用 stringsz调用 check_size
 

使用 placeholders 名称空间

名字 _n 定义在名称空间 placeholders 中,这个名称空间本身被嵌套在 std 名称空间中。用using声明可以简写占位符的名字: 
如果嫌麻烦的话,就在函数中使用 using 指令(using directive):
using 指令将使得 std::placeholders 中的名字都可见,这样就不需要再使用完全限定名了。
bind 函数一样,placeholders 名称空间定义在 functional 头文件中。
 

bind 的参数

bind 函数可以用于固定参数的值。甚至,bind 还可以用于重排序参数:
将产生一个有两个参数的新的可调用对象 g。其中 g 的第一个参数被传递给 f 的第五个参数,g 的第二个参数将被传递给 f 的第四个参数。效果相当于 g(_1, _2) 与 f(a, b, _2, c, _1) 一样,当调用 g(X, Y) 时与 f(a, b, Y, c, X) 效果一样。
 
还有 bind 可以对一个函数的参数进行重排序:
在第一个调用中,sort 调用的正常的 isShorter ,其含义是较短的字符串在前面。第二个调用中,sort 调用的反向的 isShorter ,其含义是较长的字符串在前面。
 

绑定引用形参

通常调用 bind 时,非占位符参数时拷贝到生成的调用对象中的。然而有时我们希望这个参数是按引用绑定的:
由于 os 不能被拷贝,所以不能直接 bind 这个参数。如果想要以引用方式进行绑定需要调用 ref 函数:
ref将返回一个对象其本身是可以被拷贝的,然而它的内部包含了给定参数对象的引用。同时还有一个 cref 函数用于生成一个类以容纳 const 引用。refcref 都定义在 functional 头文件中。
 
向后兼容:绑定参数
早期的C++版本对于绑定参数到函数有诸多限制,并且更加复杂。标准库定义了 bind1stbind2nd,在新标准下应该使用 bind 函数。
 
  • C++
  • 泛型算法迭代器和算法结构
    目录