向算法传递函数
谓词
谓词是一个可调用的表达式,其返回结果是一个能用作条件的值。标准库算法使用的谓词分为两类:
- 一元谓词,意味着它们只接受单一参数。
- 二元谓词,意味着它们有两个参数。
接受谓词参数的算法对输入序列中的元素调用谓词,因此,元素类型必须能够转换为谓词的参数类型。
接受一个二元谓词参数的sort:
bool isShorter(const string &s1, const string &s2)
{
return s1.size() < s2.size();
}
sort(words.begin(),words.end(),isShorter);
排序算法
将 words 按照大小重排后,希望具有相同长度的元素按照字典顺序排列,可以使用stable_sort 算法。
stable_sort(words.begin(), words.end(), isShorter);
lambda 表达式
可以向算法传递任何可调用对象,对于一个对象或一个表达式,如果可以对其使用调用运算符,则称它为可调用对象或表达式。
- 一个
lambda
表达式表示一个可调用的代码单元,可以将其理解为一个未命名的内联函数。 - 一个
lambda
具有一个返回类型,一个参数列表和一个函数体。 - 与函数不同,
lambda
可以定义在函数内部。
lambda
表达式的具体形式如下:
[capture list](parameter list)->return type{function body}
capture list
(补获列表)是一个lambda
所在函数中定义的局部变量的列表。return type
表示返回类型。parameter list
表示参数列表。function body
表示函数体。
与普通函数不同的是,lambda
必须使用尾置返回来指定返回的类型。
可以忽略参数列表和返回类型,但是必须永远包含补获列表和函数体。
//定义一个lambda表达式,返回常量42
auto f = [] { return 42; };
//调用时使用调用运算符
cout << f() << endl;
如果忽略返回类型,lambda
会根据函数体中的代码推断出返回类型,但是如果lambda的函数体中包含任何单一return
语句之外的内容,且未指定返回类型,则返回 void
。
向lambda传递参数
调用一个 lambda
时会使用给定的实参来初始化形参,通常,实参类型与形参类型必须匹配。
与普通函数不同,lambda
表达式不能有默认参数,一个 lambda
调用的实参数目永远与形参数目相等。
stable_sort(words.begin(),words.end(),
[](const string &a, const string &b)
{return a.size() < b.size(); });
使用补获列表
lambda
可以出现在一个函数中,通过将局部变量包含在补获列表中来指明将会使用这些变量。
auto wc = find_if(words.begin(),words.end(),
[sz](const string &a) {return a.size() >= sz; });
find_if
调用返回第一个长度不小于给定参数 sz
的元素。如果这个参数不存在,则返回 words.end()
的一个拷贝。
打印 words
中长度大于等于 sz
的元素:
for_each(wc,words.end(),
[](const string &s){cout<<s<<" ";});
注意:
补获列表只用于局部非静态变量,lambda
可以直接使用局部静态变量和它所在函数之外声明的名字。
lambda 补获和返回
值捕获
变量的捕获方式也可以是指或引用。
与传值参数类似,采用值捕获的前提是变量可以拷贝,与参数不同,被捕获的值是在 lambda
创建时拷贝,而不是调用时拷贝。
void func1()
{
size_t v1 = 42;
auto f = [v1] {return v1; };
v1 = 0;
auto j = f(); //j为42,f保存了创建它时的v1的拷贝
}
引用捕获
void func1()
{
size_t v1 = 42;
auto f2 = [&v1] {return v1; };
v1 = 0;
auto j = f2(); //j为0,f2保存了v1的引用而不是v1的拷贝
}
引用捕获与返回引用有着相同的问题和限制,如果采用引用捕获一个变量,就必须确保被引用的对象在 lambda
执行的时候是存在的。lambda
捕获的都是局部变量,这些变量在函数结束后就不付存在了,如果 lambda
可能在函数结束后执行,捕获的引用指向的局部变量已经消失。
可以从一个函数返回 lambda
,函数可以直接返回一个可调用对象,或者返回一个类对象,该类含有可调用对象的数据成员,如果函数返回一个 lambda
,则与函数不能返回一个局部变量的引用类似,此 lambda
也不能包含引用捕获。
注意:
- 捕获一个普通变量,通常情况下采用简单的值捕获,在此情况下,只需关注变量在捕获时是否有我们所需的值。
- 如果捕获一个指针或迭代器或引用,就必须确保在
lambda
执行时,绑定到迭代器、指针、引用的对象仍然存在,另外需要注意lambda创建到执行时这些值可能被修改了。 - 一般来说,尽量减少捕获的数据量,也尽量避免捕获指针或引用。
隐式捕获
可以让编译器根据 lambda 函数体中的代码来推断使用哪些变量:
=
指示编译器按值捕获。&
指示编译器按引用捕获。
//按值捕获,隐式捕获
wc = find_if(words.begin(), words.end(), [=]
(const string &s) {return s.size() >= sz; })
如果希望一部分变量按值捕获,其它变量按引用捕获,可以混合使用隐式捕获和显示捕获:
//os隐式捕获,引用捕获方式,c显示捕获,值捕获方式
for_each(words.begin(), words.end(), [&, c](const string &s) {os << s << c; });
//os显式捕获,引用捕获方式,c隐捕获,值捕获方式
for_each(words.begin(), words.end(), [=, &osc](const string &s) {os << s << c; });
注意:
- 当使用混合捕获模式时,捕获列表中的第一个元素必须是
&
或者=
来指定默认的捕获方式。 - 当使用混合捕获模式时,显示捕获的变量必须与隐式捕获的变量采用不同的方式,如果隐式捕获采用引用方式,则显示捕获必须采用值方式,如果,如果隐式捕获采用值方式,则显示捕获必须采用引用方式。
可变 lambda
对于一个值拷贝的变量,默认情况下 lambda
不会改变它的值,如果希望能够改变一个被捕获的变量的值,就必须在参数列表首加上关键字 mutable
:
void func1()
{
size_t v1 = 42;
auto f = [v1] () mutable {return v1++; };
v1 = 0;
auto j = f();
}
一个引用捕获的变量能否修改它的值则依赖于此引用指向的是一个 const
类型还是一个非 const
类型。
void func1()
{
size_t v1 = 42;
auto f = [&v1] () {return v1++; };
v1 = 0;
auto j = f();
}
指定 lambda返回类型
默认情况下,如果一个 lambda
函数体包含 return
之外的任何语句,则编译器假定此 lambda
返回 void
。与其他返回 void
的函数类似,被推断返回 void
的 lambda
不能返回值。
返回一个序列中所有数据的绝对值:
transform(vi.begin(), vi.end(), vi.begin(), [](int i) {return i < 0 ? -i : i; });
上面的表达式是正确,但是如果改写成:
transform(vi.begin(), vi.end(), vi.begin(), [](int i) {if (< 0)return -i; else return i; });
将不能推断返回类型,编译器将返回 void
类型。
当需要为 lambda 指定返回类型的时候,必须使用尾置返回类型:
transform(vi.begin(), vi.end(), vi.begin(), [](int i) ->int {if (< 0)return -i; else return i; });
参数绑定
- 对于只在一两个地方使用的简单操作,
lambda
表达式最有用,但是如果需要在很多地方使用,通常应该定义成函数,而不是多次编写lambda
表达式。 - 如果一个操作需要很多语句才能完成,通常使用函数更好。
- 如果
lambda
的捕获列表为空,通常可以用函数来代替它;但是对于捕获局部变量的lambda
,用函数来替换就不那么容易。
例如:
auto wc = find_if(words.begin(),words.end(),
[sz](const string &a) {return a.size() >= sz; });
可以很容易写出一个函数:
bool check_size(const string &s,string::size_type sz)
{return s.size() >= sz;}
但是不能将 check_size
直接作为 find_if
的一个参数,因为 find_if
接受一个一元谓词,因此传递给 find_if
的可调用对象必须接受单一参数。为了使用 check_size
来代替此 lambda
必须解决如何向 sz
形参传递一个参数的问题。
标准库 bind 函数
bind
函数定义在头文件 functional
中,可以将 bind
函数看作一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来适应原对象的参数列表。
bind
函数的一般形式为:
auto newCallable = bind(callable,arg_list);
当调用 newCallable
时,newCallable
会调用 callable
,并传递给它 arg_list
参数。
arg_list
中的参数可能包含形如 _n
的名字,其中 n
是一个整数,这些参数是占位符,表示 newCallable
的参数,它们占据了传递给 newCallable
的参数位置,数值 n
表示生成的可调用对象中参数的位置:_1
为newCallable
的第一个参数,_2
为 newCallable
的第二个参数,以此类推。
绑定 check_size 的 sz 参数
auto check6 = bind(check_size,_1, 6);
此 bind
调用只有一个占位符,表示 check6
只接受单一参数。
占位符出现在 arg_list
的第一个位置,表示 check6
的此参数对应 check_size
的第一个参数。此参数是一个 const string&
。因此,调用 check6
必须传递给它一个 string
类型的参数,check6
会将此参数传递给 check_size
。
string s = "hello";
bool b1 = check6(s); //check6 会调用check_size(s,6)
如此,可以替换原来的 lambda 表达式:
auto wc = find_if(words.begin(),words.end(),
bind(check_size,_1,sz));
bind
调用生成一个可调用对象,将 check_size
的第二个参数绑定到 sz
的值。
使用 placeholders 名字
名字 _n
都定义在一个名为 placeholdaers
的命名空间中,而这个命名空间本身定义在 std
命名空间。为了使用这个名字,两个命名空间都需要写:
using std::placeholders::_1;
除了对每一个使用的名字单独 using
声明外,也可以统一声明:
using namespace std::placeholders;
placeholdaers
命名空间定义在 functional
头文件中。
如果第一个调用类似:isShorter(A,B)
,则第二个调用为:isShorter(B,A)
。
绑定引用参数
默认情况下,bind
的那些不是占位符的参数被拷贝到 bind
返回的可调用对象中,但是与 lambda
类似,有时对有些绑定的参数希望以引用 得方式传递,或是绑定的参数无法拷贝。
for_each(words.begin(),words.edn(),[&os,c](const string) &s {os<<s<<c;});
可以编写函数替换lambda表达式:
ostream & print(ostream &os,cpnst string &s,char c)
{
return os<<s<<c;
}
但是不能直接使用 bind
来代替 os
的捕获:
//错误,ostream 不可以拷贝
for_each(words.begin(),words.edn(),bind(print,os,_1,' '));
如果想传递给bind一个对象而又不拷贝它,则必须使用标准库 ref
函数:
for_each(words.begin(),words.edn(),bind(print,ref(os),_1,' '));
ref
函数返回一个对象,包含给定的引用,此对象是可以拷贝的,标准库还有一个 cref
函数,生成一个保存 const
引用的类。
ref
、cref
也都定义在头文件 functional
中。