文章目录
可调用对象
可调用对象,有函数、函数指针、重载了函数调用运算符的类(也就是仿函数)、lambda表达式四种形式。这里主要介绍lambda的作用。
谓词
先看下面的代码:
static bool cmp(const int& a, const int& b) { return a > b; }
int main() {
vector<int> arr{ 1, 4, 5, 2 };
sort(arr.begin(), arr.end(), cmp);
for (auto w : arr) {
cout << w << " ";
}
}
由于STL中的sort函数默认是less(升序排序),这里为了演示就调用了sort的重载版本,传入一个二元谓词版本。这里先解释下谓词是什么?
谓词是一个可调用的表达式(也就是可调用对象),其返回结果是一个能用作条件的值。标准库算符所使用的谓词分为:一元谓词(只接受单一参数),和二元谓词(接受两个参数),没有三元、四元谓词。接受谓词参数的算法对输入序列中的元素调用谓词。因此,元素类型必须能转换为谓词的参数类型。
这里再来看上面的cmp函数为什么要加static声明,其实在上面的例子中加不加都能通过。但是最好还是加上,因为如果在一个类中定义了cmp函数,如果在成员函数中调用这个带谓词的sort函数会调用失败,因为std::sort是一个全局函数,其传入的谓词要是全局或者是静态的。
由于谓词只有一元和二元之分,那么有时候希望进行的操作需要更多的参数,超出了算法对谓词的限制。下面有个需求,找出vector中字符长度大于length的第一个元素。find_if是find的一元谓词版本,其需要一对迭代器和一元谓词,该函数返回第一个使谓词返回非0值得元素,如果不存在这样得元素,则返回尾迭代器。
static bool cmp(const string& s) { return s.size() < 4; }
int main() {
vector<string> arr{ "hello", "world!", "C++", "Welcome", "you" };
auto it = find_if(arr.begin(), arr.end(), cmp);
cout << *it << endl;
}
// 结果显式:C++
这里规定了比较的长度为4,如果想传入一个长度变量怎么办?但是STL规定了传递给find_if任何函数都必须严格接受一个参数,所以没有任何办法能传递给它两个参数。下面可以用lambda来实现(lambda也是传入一个参数,但是其通过其它功能来得到长度变量)
lambda
lambda可以理解成一个未命名的内联函数。与任何函数类似,一个lambda具有一个返回类型、一个参数列表和一个函数体。但是与别的函数不同,lambda可能定义在函数内部。
[capture list](parameter list) mutable或异常exception ->return type { function body }
- [capture list]:捕获列表,是一个lambda所在函数中可以访问的变量的列表(通常为空)。其总是出现在lambda函数的开始处。事实上,[]是lambda引出符。编译器根据该引出符判断接下来的代码是否是lambda函数。
- (parameter list):参数列表,与普通函数一样,但是如果不需要参数传递,则可以连同括号()一起省略。
- mutable:修饰符,默认情况下lambda捕获的变量是按值传递的,不能被改变,如果加上mutable修饰符可以改变其变量。在使用该修饰符时,参数列表不可省略。
- 异常说明exception:用于指定函数抛出的异常,如抛出整数类型的异常,可以使用throw(int)。
- ->return-type:返回类型。处于方便,不需要返回值的时候也可以连同符号->一起省略。此外,在返回类型明确的情况下,也可以省略该部分,让编译器对返回类型进行推导。
- { function body }:函数体。内容与普通函数一样,不过除了可以使用参数之外,还以可以使用所有捕获的变量。
下面先通过lambda实现上述的find_if,然后在根据lambda形式介绍各个部分。
int main() {
int length = 4;
vector<string> arr{ "hello", "world!", "C++", "Welcome", "you" };
auto lambda = [length](string& s) { return s.size() < length; };
auto it = find_if(arr.begin(), arr.end(), lambda);
cout << *it << endl;
}
// 结果显式:C++
捕捉列表
其可以为空,但是不可以省略。其可以捕获当前函数中上下文数据,描述了上下文中哪些数据可以被lambda使用,以及使用方式(按值还是按引用传递)。需要注意的是,lambda仅能捕捉当前函数作用域中的自动变量,不能捕捉static变量或者全局变量,局部变量只有在捕捉列表中捕获了,才可以在lambda函数体中使用(其含义是lambda函数体中可以直接使用局部static变量和它所在函数之外声明的名字)。
语法上,捕捉列表由多个捕捉项组成,并以逗号分割。捕捉列表有如下几种形式:
- [var] 表示值传递方式捕捉变量var。
- [=] 表示值传递方式捕捉所有当前作用域的变量(包括this)
- [&var] 表示引用传递捕捉变量var
- [&] 表示引用传递捕捉所有当前作用域的变量(包括this)
- [this] 表示值传递方式捕捉当前的this指针
- [=, &a, &b] 表示以引用传递的方式捕捉变量a和b,值传递方式捕捉其它所有变量
- [&, a, this] 表示以值传递的方式捕捉变量a和this,引用传递方式捕捉其他所有变量
- [=, a] 这里 = 已经以值传递方式捕捉了所有变量,捕捉a重复
- [&, &this] 同理
值捕获
与传值参数类型,采用值捕获的前提是变量可以拷贝。以参数不同,被捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝。
int main() {
int length = 4;
vector<string> arr{ "hello", "world!", "C++", "Welcome", "you" };
auto lambda = [length](string& s) { return s.size() < length; };
length = 7;
auto it = find_if(arr.begin(), arr.end(), lambda);
cout << *it << endl;
}
// 仍旧输出:C++
// 由于被捕获变量的值是在lambda创建时拷贝,因此随后对其修改不会影响到lambda内对应的值
引用捕获
以引用捕获,即可以在lambda函数中进行修改该值。由于可以从函数返回一个lambda,则与函数不能返回一个局部变量的引用类似,此lambda不能包含引用捕获(因为引用捕获的局部变量在函数退出时已经销毁了)
int main() {
int length = 4;
vector<string> arr{ "hello", "world!", "C++", "Welcome", "you" };
auto lambda = [&length](string& s) { length = 10; return s.size() < length; };
auto it = find_if(arr.begin(), arr.end(), lambda);
cout << length << endl;
}
// 输出:10
可变lambda:mutable
默认情况下,对于一个值被拷贝的变量,lambda不会改变其值。如果希望能改变一个被捕捉的变量的值,就必须在参数列表后加上mutable修饰符。在使用该修饰符时,参数列表不可省略。
int main() {
int length = 4;
vector<string> arr{ "hello", "world!", "C++", "Welcome", "you" };
auto lambda = [length](string& s) mutable { length = 10; return s.size() < length; };
auto it = find_if(arr.begin(), arr.end(), lambda);
cout << length << endl;
}
异常说明
lambda如果没有显式声明异常说明符,其默认是throw (),即不抛出任何异常。当然也可以显式throw(int)异常。
int main() {
auto lambda = []() throw(int) {
throw(5);
};
try {
lambda();
}
catch (int&) {
cout << "捕捉到了int类型异常" << endl;
}
}
指定lambda返回类型
上述代码中,都没有显式指定lambda的返回类型,都由编译器自行判断。
auto lambda = [length](string& s) { return s.size() < length; }; // 返回bool
auto lambda = []() throw(int) { throw(5); }; // 返回void
那么什么时候需要指定lambda然会类型呢,当函数体中有两个return时,且这两个return返回值类型不同。
auto lambda = [length](string& s) {
if (length < 5) return s.size() < length;
else return 1;
};
// 编译出错,因为return 1返回int, return s.size() < length返回bool类型
// 如果都是返回bool类型,是不会报错的,这个时候可以显式指定返回类型
auto lambda = [length](string& s) ->bool {
if (length < 5) return s.size() < length;
else return 1;
};
lambda 捕捉表达式
前面讲过,lambda表达式可以按复制或者引用捕获在其作用域范围内的变量。而有时候,我们希望捕捉不在其作用域范围内的变量,而且最重要的是我们希望捕捉右值。所以C++14中引入了表达式捕捉,其允许用任何类型的表达式初始化捕捉的变量。
int main() {
auto lambda = [str = "hello,world!"]{ return str; };
cout << lambda() << endl;
}
// 输出hello,world
int x = 4;
int main() {
auto lambda = [&r = x, x = x + 1]{ r += 2; return x*x; };
cout << lambda() << endl;
cout << x << endl;
}
// 输出:25,6
// r是x的引用,所以r变了,外面的x也变了;而捕捉列表的x = x+1 = 5,相当于值传递与外界的全局x没有关系
int main() {
unique_ptr<int> p = make_unique<int>(5);
auto lambda = [v = std::move(p)]{ return *v; };
cout << lambda() << endl;
}
// 由于unique_ptr是不能拷贝的,所以只能通过移动语义来获取右值
泛型lambda
从C++14 开始,Lambda 函数的形式参数可以使用auto关键字来产生意义上的泛型。
int main() {
auto lambda = [=](auto x, auto y){ return x*y; };
cout << lambda(3,4.5) << endl;
}
constexpr lambda
从C++17开始,如果lambda表达式符合要求的话,该表达式会隐式转换为constexpr表达式。(表达式内没有静态变量,没有虚函数,没有 try/catch语句, 没有new/delete关键字)
// gcc version 8.2.0 (MinGW.org GCC-8.2.0-5)
int main() {
auto lambda = [](auto x) { return x; };
int arr[lambda(1)] = {1};
cout << arr[0] << endl;
}
// 输出:1
题外话,当初下载了vs2017也没怎么管啥具体版本,但是今天才发现vs2017对C++17支持并不是全部,具体可以看vs微软官方文档,查了下constexpr lambda需要vs2017.15.3版本,而我的版本是15.0。。
上述程序中可以看到,lambda隐式转成了constexpr,如果不确定是否可以转成constexpr,可以加constexpr修饰符来判断
int main() {
static int x = 5;
auto lambda = []() constexpr { static int y = 5; return x; }; // error
int arr[lambda()] = {1};
cout << arr[0] << endl;
}
向lambda传递this的拷贝
如果成员函数内有lambda表达式,在表达式内希望调用其他成员函数或变量时,我们需要在[]中添加捕捉对象说明。
class Test {
private:
string name = "hello";
public:
void foo() {
auto l1 = [] { cout << name << endl; };
auto l2 = [=] { cout << name << endl; };
auto l3 = [&] { cout << name << endl; };
l1();
l2();
l3();
}
};
int main() {
Test t;
t.foo();
}
然而这有一个问题,就是说如果lambda表达式在调用时,传递进来的对象已经销毁了,这样就会引发错误。
class Test {
private:
string name = "hello";
public:
thread startThread() const {
thread t(
[this] {
cout << "I will shellp 3 seconds" << endl;
this_thread::sleep_for(std::chrono::seconds(3));
cout << name << endl;
}
);
return t;
}
};
int main() {
std::thread t;
{
Test d;
t = d.startThread();
} // d已经销毁
cout << "the main thread wait for sub thread end." << endl;
t.join();
}
// 由于d对象已经销毁了,所以cout << name << endl是未定义的
C++17中,我们可以在lambda表达式的捕获类别里[]写上*this,表示传递到lambda中的是this对象的拷贝。从而解决上述的问题。
class Test {
private:
string name = "hello";
public:
thread startThread() const {
thread t(
[*this] {
cout << "I will shellp 3 seconds" << endl;
this_thread::sleep_for(std::chrono::seconds(3));
cout << name << endl;
}
);
return t;
}
};
int main() {
std::thread t;
{
Test d;
t = d.startThread();
} // d已经销毁
cout << "the main thread wait for sub thread end." << endl;
t.join();
}
参考资料
(1)《C++Primer》
(2)《深入理解C++11·C++11新特性解析与应用》
(3)《快速上手C++11/14》
(4)https://blog.youkuaiyun.com/Hello_World_156_5634/article/details/90670637
(5)https://www.cnblogs.com/AlainGao/p/10675201.html
(6)https://blog.youkuaiyun.com/janeqi1987/article/details/100176985