3.1 Lambda 表达式

Lambda 表达式是现代 C++ 中最重要的特性之一,而 Lambda 表达式,实际上就是提供了一个类似匿名函数的特性, 而匿名函数则是在需要一个函数,但是又不想费力去命名一个函数的情况下去使用的。这样的场景其实有很多很多, 所以匿名函数几乎是现代编程语言的标配。

3.1.1 基础

Lambda 表达式的基本语法如下:

  1. [捕获列表](参数列表) mutable(可选) 异常属性 -> 返回类型 {
  2. // 函数体
  3. }

上面的语法规则除了 [捕获列表] 内的东西外,其他部分都很好理解,只是一般函数的函数名被略去, 返回值使用了一个 -> 的形式进行(我们在上一节前面的尾返回类型已经提到过这种写法了)。

所谓捕获列表,其实可以理解为参数的一种类型,Lambda 表达式内部函数体在默认情况下是不能够使用函数体外部的变量的, 这时候捕获列表可以起到传递外部数据的作用。根据传递的行为,捕获列表也分为以下几种:

1.值捕获

与参数传值类似,值捕获的前提是变量可以拷贝,不同之处则在于,被捕获的变量在 Lambda 表达式被创建时拷贝, 而非调用时才拷贝:

  1. void lambda_value_capture()
  2. {
  3. int value = 1;
  4. auto copy_value = [value] {
  5. return value;
  6. };
  7. value = 100;
  8. auto stored_value = copy_value();
  9. std::cout << "stored_value = " << stored_value << std::endl;
  10. // 这时, stored_value == 1, 而 value == 100.
  11. // 因为 copy_value 在创建时就保存了一份 value 的拷贝
  12. }


2. 引用捕获**

与引用传参类似,引用捕获保存的是引用,值会发生变化。

  1. void lambda_reference_capture() {
  2. int value = 1;
  3. auto copy_value = [&value] {
  4. return value;
  5. };
  6. value = 100;
  7. auto stored_value = copy_value();
  8. std::cout << "stored_value = " << stored_value << std::endl;
  9. // 这时, stored_value == 100, value == 100.
  10. // 因为 copy_value 保存的是引用
  11. }

3. 隐式捕获

手动书写捕获列表有时候是非常复杂的,这种机械性的工作可以交给编译器来处理,这时候可以在捕获列表中写一个 & 或 = 向编译器声明采用引用捕获或者值捕获.

总结一下,捕获提供了 Lambda 表达式对外部值进行使用的功能,捕获列表的最常用的四种形式可以是:

  • [] 空捕获列表
  • [name1, name2, ...] 捕获一系列变量
  • [&] 引用捕获, 让编译器自行推导引用列表
  • [=] 值捕获, 让编译器自行推导值捕获列表

4. 表达式捕获
**

这部分内容需要了解后面马上要提到的右值引用以及智能指针

上面提到的值捕获、引用捕获都是已经在外层作用域声明的变量,因此这些捕获方式捕获的均为左值,而不能捕获右值。

C++14 给与了我们方便,允许捕获的成员用任意的表达式进行初始化,这就允许了右值的捕获, 被声明的捕获变量类型会根据表达式进行判断,判断方式与使用 auto 本质上是相同的:

  1. #include <iostream>
  2. #include <utility>
  3. int main()
  4. {
  5. auto important = std::make_unique<int>(1);
  6. auto add = [v1 = 1, v2 = std::move(important)](int x, int y) -> int {
  7. return x + y + v1 + (*v2);
  8. };
  9. std::cout << add(3, 4) << std::endl;
  10. return 0;
  11. }

在上面的代码中,important 是一个独占指针,是不能够被捕获到的,这时候我们需要将其转移为右值, 在表达式中初始化。

3.2.2 泛型 Lambda

上一节中我们提到了 auto 关键字不能够用在参数表里,这是因为这样的写法会与模板的功能产生冲突。 但是 Lambda 表达式并不是普通函数,所以 Lambda 表达式并不能够模板化。 这就为我们造成了一定程度上的麻烦:参数表不能够泛化,必须明确参数表类型。

幸运的是,这种麻烦只存在于 C++11 中,从 C++14 开始, Lambda 函数的形式参数可以使用 auto 关键字来产生意义上的泛型:

  1. auto add = [](auto x, auto y) {
  2. return x + y;
  3. };
  4. add(1, 2);
  5. add(1.1, 2.2);

3.2 函数对象包装器

这部分内容虽然属于标准库的一部分,但是从本质上来看,它却增强了 C++ 语言运行时的能力, 这部分内容也相当重要,所以放到这里来进行介绍。

3.2.1 std::function

Lambda 表达式的本质是一个和函数对象类型相似的类类型(称为闭包类型)的对象(称为闭包对象), 当 Lambda 表达式的捕获列表为空时,闭包对象还能够转换为函数指针值进行传递,例如:

  1. #include <iostream>
  2. using foo = void(int); // 定义函数类型, using 的使用见上一节中的别名语法
  3. void functional(foo f)
  4. { // 定义在参数列表中的函数类型 foo 被视为退化后的函数指针类型 foo*
  5. f(1); // 通过函数指针调用函数
  6. }
  7. int main()
  8. {
  9. auto f = [](int value) {
  10. std::cout << value << std::endl;
  11. };
  12. functional(f); // 传递闭包对象,隐式转换为 foo* 类型的函数指针值
  13. f(1); // lambda 表达式调用
  14. return 0;
  15. }

上面的代码给出了两种不同的调用形式,一种是将 Lambda 作为函数类型传递进行调用, 而另一种则是直接调用 Lambda 表达式,在 C++11 中,统一了这些概念,将能够被调用的对象的类型, 统一称之为可调用类型。而这种类型,便是通过 std::function 引入的。

C++11 std::function 是一种通用、多态的函数封装, 它的实例可以对任何可以调用的目标实体进行存储、复制和调用操作, 它也是对 C++ 中现有的可调用实体的一种类型安全的包裹(相对来说,函数指针的调用不是类型安全的), 换句话说,就是函数的容器。当我们有了函数的容器之后便能够更加方便的将函数、函数指针作为对象进行处理。 例如:

  1. #include <functional>
  2. #include <iostream>
  3. int foo(int para)
  4. {
  5. return para;
  6. }
  7. int main()
  8. {
  9. // std::function 包装了一个返回值为 int, 参数为 int 的函数
  10. std::function<int(int)> func = foo;
  11. int important = 10;
  12. std::function<int(int)> func2 = [&](int value) -> int {
  13. return 1 + value + important;
  14. };
  15. std::cout << func(10) << std::endl;
  16. std::cout << func2(10) << std::endl;
  17. }

3.2.2 std::bind 和 std::placeholder

std::bind 则是用来绑定函数调用的参数的, 它解决的需求是我们有时候可能并不一定能够一次性获得调用某个函数的全部参数,通过这个函数, 我们可以将部分调用参数提前绑定到函数身上成为一个新的对象,然后在参数齐全后,完成调用。 例如:

  1. int foo(int a, int b, int c)
  2. {
  3. ;
  4. }
  5. int main()
  6. {
  7. // 将参数1,2绑定到函数 foo 上,但是使用 std::placeholders::_1 来对第一个参数进行占位
  8. auto bindFoo = std::bind(foo, std::placeholders::_1, 1, 2);
  9. // 这时调用 bindFoo 时,只需要提供第一个参数即可
  10. bindFoo(1);
  11. }

示:注意 auto 关键字的妙用。有时候我们可能不太熟悉一个函数的返回值类型, 但是我们却可以通过 auto 的使用来规避这一问题的出现。

3.3 右值引用

右值引用是 C++11 引入的与 Lambda 表达式齐名的重要特性之一。它的引入解决了 C++ 中大量的历史遗留问题, 消除了诸如 std::vectorstd::string 之类的额外开销, 也才使得函数对象容器 std::function 成为了可能。

3.3.1 左值、右值的纯右值、将亡值、右值

要弄明白右值引用到底是怎么一回事,必须要对左值和右值做一个明确的理解。

  • 左值(lvalue, left value),顾名思义就是赋值符号左边的值。准确来说, 左值是表达式(不一定是赋值表达式)后依然存在的持久对象。

  • 右值(rvalue, right value),右边的值,是指表达式结束后就不再存在的临时对象。

而 C++11 中为了引入强大的右值引用,将右值的概念进行了进一步的划分,分为:纯右值、将亡值。

  • 纯右值(prvalue, pure rvalue),纯粹的右值,要么是纯粹的字面量,例如 10, true; 要么是求值结果相当于字面量或匿名临时对象,例如 1+2。非引用返回的临时变量、运算表达式产生的临时变量、 原始字面量、Lambda 表达式都属于纯右值。

需要注意的是,字符串字面量只有在类中才是右值,当其位于普通函数中是左值。例如:

  1. class Foo {
  2. const char*&& right = "this is a rvalue"; // 此处字符串字面量为右值
  3. public:
  4. void bar()
  5. {
  6. right = "still rvalue"; // 此处字符串字面量为右值
  7. }
  8. };
  9. int main()
  10. {
  11. const char* const& left = "this is an lvalue"; // 此处字符串字面量为左值
  12. }
  • 将亡值(xvalue, expiring value),是 C++11 为了引入右值引用而提出的概念(因此在传统 C++ 中, 纯右值和右值是同一个概念),也就是即将被销毁、却能够被移动的值。

将亡值可能稍有些难以理解,我们来看这样的代码:

  1. std::vector<int> foo()
  2. {
  3. std::vector<int> temp = { 1, 2, 3, 4 };
  4. return temp;
  5. }
  6. std::vector<int> v = foo();

在这样的代码中,就传统的理解而言,函数 foo 的返回值 temp 在内部创建然后被赋值给 v, 然而 v 获得这个对象时,会将整个 temp 拷贝一份,然后把 temp 销毁,如果这个 temp 非常大, 这将造成大量额外的开销(这也就是传统 C++ 一直被诟病的问题)。在最后一行中,v 是左值、 foo() 返回的值就是右值(也是纯右值)。但是,v 可以被别的变量捕获到, 而 foo() 产生的那个返回值作为一个临时值,一旦被 v 复制后,将立即被销毁,无法获取、也不能修改。 而将亡值就定义了这样一种行为:临时的值能够被识别、同时又能够被移动。

在 C++11 之后,编译器为我们做了一些工作,此处的左值 temp 会被进行此隐式右值转换, 等价于 static_cast<std::vector<int> &&>(temp),进而此处的 v 会将 foo 局部返回的值进行移动。 也就是后面我们将会提到的移动语义。