C++中一共有5种调用对象:函数,函数指针,重载了函数调用运算符的类(仿函数),bind创建的对象 和 lambda表达式。
函数指针
仿函数
lambda表达式
没有lambda的话,函数对象的定义太麻烦了,你得定义一个类,重载operator(),然后再创建这个类的实例。所以lambda表达式可以看成是函数对象的语法糖,在你需要的时候,它可以很简洁地给你生成一个函数对象。
语法格式
[capture list] (param list) -> return type { function body }
[capture list]
是一个所在函数中定义的局部变量(非static)的列表,param list
, return type
,function body
和普通的函数一样表示返回类型(尾置返回),参数列表,和函数体。
我们可以忽略参数列表和返回类型,但必须包含捕获列表和函数体。
auto f = [] { return 42; } // 必须包含捕获列表和函数体。
当定义一个lambda时,编译器生成一个与lambda对应的新的(未命名的)类类型。默认情况下,lambda生成的类都包含一个对应该lambda所捕获的变量的数据成员,在lambda创建时被初始化。
向lambda传递参数
与一个普通函数类似,调用一个lambda时给定的实参被用来初始化lambda的形参。通常,实参和形参类型必须匹配。但与普通函数不同,lambda不能有默认函数参数。
使用捕获列表
- 值捕获
采用值捕获的前提是 变量可以被拷贝,另外被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝void func { int v1 = 42; auto f = [v1] { return v1; } v1 = 0; auto j = f(); // j = 42, v1在lambda创建时拷贝,而不是调用时拷贝 }
- 引用捕获
采用引用捕获时,必须确保被引用的对象在lambda执行的时候是存在的(lambda捕获的都是局部变量,这些变量在函数结束后就不存在了)void func() { int val = 42; auto f = [&val]() { return val; }; val = 0; auto j = f(); // j = 0,引用捕获。 }
- 隐式捕获
除了显示列出我们希望使用来自函数的变量之外,我们可以让编译器隐式推断lambda体中的代码来推断我们使用了哪些变量。为了指示编译器推断捕获列表,我们应在捕获列表写&和=,- &告诉编译器采用引用捕获方式
- =表示采用值捕获的方式。
wc = find_if(words.begin(), words.end(), [=](const string &s) { return s.size() > sz; })
可变lambda
- mutable
对于lambda表达式。默认情况下,lambda不会改变其值,如果我们希望能改变一个被捕获变量的值,就必须在参数列表首加上关键字mutable。void func() { int val = 42; // auto f = [val]() mutable { return ++val; }; // 编译错误,v1只读。 auto f = [val]() mutable { return ++val; }; // 加上mutable关键字,编译正确。 int j = f(); // j = 43 cout << j << " " << val << endl; // 输出 43 和 42 }
加上mutable关键字后,值捕获也会改变被捕获变量的值。
返回类型
默认情况下,如果一个lambda体包含return之外的任何语句,编译器假定此lambda返回void。所以此时我们就要显示指定尾指返回类型。
transfrom(v.begin(), v.end(),
[](int i) {
if (i < 0)
return -i;
else
return i;
}) // 错误
transfrom(v.begin(), v.end(),
[](int i) -> int {
if (i < 0)
return -i;
else
return i;
})
参数绑定bind函数
我们需要在一个std::vector<std::string>
中寻找大于某长度单词。那么我们可以这样写:
auto it = find_if(vec.begin(), vec.end(),
[](const std::string& s) {
return (s.size() > 5);
});
同样,我们可以用函数去实现。
bool check_size(const std::string& s)
{
return s.size() > 5;
}
auto it = find_if(vec.begin(), vec.end(), check_size);
但假设,我们想要指定长度来筛选。那用函数是不能实现的。
std::string::size_type sz = 5;
auto it = find_if(vec.begin(), vec.end(),
[sz](const std::string& s) {
return (s.size() > sz);
});
bool check_size(const std::string& s, std::string::size_type sz)
{
return s.size() > sz;
}
但是这个函数不用你管作为find_if
的参数,因为find_if
接受的是一元谓词。
// find_if 可能实现
template<class InputIt, class UnaryPredicate>
InputIt find_if(InputIt first, InputIt last, UnaryPredicate p)
{
for (; first != last; ++first) {
if (p(*first)) { // p只接受一个参数
return first;
}
}
return last;
}
但我们可以标准库的bind
函数,它定义在头文件functional
中,可以将它看成一个通用的函数适配器,它接受一个可调用的对象,生成一个新的可调用对象来适应原对象的参数列表
auto newCallable = bind(callable, arg_list);
那么现在可以这样写:
auto it = find_if(vec.begin(), vec.end(), bind(check_size, std::placegolders::_1, sz));
此处的bind
调用生成一个可调用对象,将check_size
的第二个参数绑定到sz
的值,当find_if
对vec中的std::string
调用这个对象时。它会将给定的参数std::string
和sz
传递给check_size
函数。
-
使用placegolders名字
名字_n
都定义在名为placegolders
的命名空间中,而这个命名空间本身定义在std
命名空间中。它表示占位符,意味着将自己第n个参数按照顺序传递给原调用对象。auto g = bind(f, a, b, std::placegolders::_2, c, std::placegolders::_1); g(x, y) == f(a, b, y, c, z);
在上面的例子中,
g
表示一个有两个参数的新的调用对象,原调用对象f
有5个参数。g
的第一个参数是f
的第5个参数,g
的第2个参数是f
的第3个参数。 -
绑定引用参数
默认情况下,bind
那些不是占位符的参数被拷贝到bind
返回的可调用对象中。但是和lambda一样,有时对绑定的参数我们希望以引用的方式传递,或是要绑定的参数类型无法拷贝。例如,我们希望在打印每个
vec
中的单词后输出一个换行。for_each(vec.begin(), vec.end(), [&os, c](const std::string& s){ os << s << c; }); // 很容易编写一个对应的函数版本: ostream& print(ostream& os, const std::string& s, char c) { return os << s << c; } // 错误:不能拷贝os for_each(vec.begin(), vec.end(), bind(print, os, _1, ' '));
那么,这时我们希望传递给
bind
的是一个对象而又不拷贝它,那么就必须使用ref
和cref
函数。它返回一个对象的引用。for_each(vec.begin(), vec.end(), bind(print, std::ref(os), _1, ' '));