[C++]现代C++语言核心特性解析 Chapter 7

lambda表达式(C++11~C++20)

1 lambda表达式语法

语法:

1
[ captures ] ( params ) specifiers exception -> ret { body }
  • [ captures ] —— 捕获列表,它可以捕获当前函数作用域的零个或多个变量,变量之间用逗号分隔。捕获列表的捕获方式有两种:按值捕获和引用捕获。
  • ( params ) —— 可选参数列表,语法和普通函数的参数列表一样,在不需要参数的时候可以忽略参数列表。
  • specifiers —— 可选限定符,C++11中可以用mutable,它允许我们在lambda表达式函数体内改变按值捕获的变量,或者调用非const的成员函数。
  • exception —— 可选异常说明符,我们可以使用noexcept来指明lambda是否会抛出异常。
  • ret —— 可选返回值类型,不同于普通函数,lambda表达式使用返回类型后置的语法来表示返回类型,如果没有返回值(void类型),可以忽略包括->在内的整个部分。另外,我们也可以在有返回值的情况下不指定返回类型,这时编译器会为我们推导出一个返回类型。
  • { body } —— lambda表达式的函数体,这个部分和普通函数的函数体一样。

2 捕获列表

2.1 作用域

捕获列表中的变量存在于两个作用域

  • lambda表达式定义的函数作用域
  • lambda表达式函数体的作用域。

前者是为了捕获变量,后者是为了使用变量。

标准规定能捕获的变量必须是一个自动存储类型。简单来说就是非静态的局部变量

如果想使用全局变量或者局部静态变量,直接使用就可以,无需捕获。

2.2 捕获值和捕获引用

捕获值

直接在捕获列表中写入变量名即可,多个变量用逗号分隔。

捕获值是将函数作用域的x和y的值复制到lambda表达式对象的内部,就如同lambda表达式的成员变量一样。

捕获引用

捕获引用的语法与捕获值只有一个&的区别,要表达捕获引用我们只需要在捕获变量之前加上&。

改变捕获值

lambda表达式捕获的变量默认为常量,或者说lambda是一个常量函数(类似于常量成员函数)。所以,值捕获的变量无法在lambda表达式内部修改,而引用捕获的变量可以修改(这是因为修改引用捕获的变量,实际是修改引用绑定的变量,而引用本身并没有被修改)。

为了修改值捕获的变量,需要用到 mutable 说明符。

1
2
3
4
5
6
7
8
9
10
void bar3()
{
int x = 5, y = 8;
auto foo = [x, y] () mutable {
x += 1;
y += 2;
return x * y;
};
std::cout << foo() << std::endl;
}

注意事项

  • 值捕获的变量是原本变量的一个副本,修改不会影响原本的变量,但是引用捕获的变量会影响原本的变量。
  • 在lambda表达式内部修改某个按值捕获的变量,每一次调用lambda表达式造成的修改,都会影响下一次的调用。
  • 捕获值的变量在lambda表达式定义的时候已经固定下来了,无论函数在lambda表达式定义后如何修改外部变量的值,lambda表达式捕获的值都不会变化

2.3 特殊的捕获方法

lambda表达式的捕获列表除了指定捕获变量之外还有3种特殊的捕获方法

  • [this] —— 捕获this指针,捕获this指针可以让我们使用this类型的成员变量和函数。
  • [=] —— 捕获lambda表达式定义作用域的全部变量的值,包括this。
  • [&] —— 捕获lambda表达式定义作用域的全部变量的引用,包括this。

3 lambda表达式的实现原理

在C++11标准中,lambda表达式的优势在于书写简单方便且易于维护,而函数对象的优势在于使用更加灵活不受限制,但总的来说它们非常相似。实际上这也正是lambda表达式的实现原理。

lambda表达式在编译期会由编译器自动生成一个闭包类,在运行时由这个闭包类产生一个对象,我们称它为闭包。在C++中,所谓的闭包可以简单地理解为一个匿名且可以包含定义时作用域上下文的函数对象。

4 无状态lambda表达式

C++标准对于无状态的lambda表达式有着特殊的照顾,即它可以隐式转换为函数指针

1
2
3
4
5
void f(void(*)()) {}
void g() { f([] {}); } // 编译成功

void f(void(&)()) {}
void g() { f(*[] {}); }

5 在STL中使用lambda表达式

STL中常常会见到这样一些算法函数,它们的形参需要传入一个函数指针或函数对象从而完成整个算法,例如std::sort、std::find_if等。

使用lambda表达式直接在传参时定义了辅助函数。无论是编写还是阅读代码,直接定义lambda表达式都比定义辅助函数更加简洁且容易理解。

6 广义捕获 C++14 (后续再看)

C++14标准中定义了广义捕获,所谓广义捕获实际上是两种捕获方式,

  1. 简单捕获,这种捕获就是前文中提到的捕获方法,即[identifier]、[&identifier]以及[this]等。

  2. 初始化捕获,这种捕获方式是在C++14标准中引入的,它解决了简单捕获的一个重要问题,即只能捕获lambda表达式定义上下文的变量,而无法捕获表达式结果以及自定义捕获变量名,比如

    1
    2
    3
    4
    5
    int main()
    {
    int x = 5;
    auto foo = [r = x + 1]{ return r; };
    }

初始化捕获在某些场景下是非常实用的

  1. 使用移动操作减少代码运行的开销,例如:

    1
    2
    3
    4
    5
    6
    7
    #include <string>

    int main()
    {
    std::string x = "hello c++ ";
    auto foo = [x = std::move(x)]{ return x + "world"; };
    }
  2. 在异步调用时复制this对象,防止lambda表达式被调用时因原始this对象被析构造成未定义的行为,比如

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Work
    {
    private:
    int value;

    public:
    Work() : value(42) {}
    std::future<int> spawn()
    {
    return std::async([=, tmp = *this]() -> int { return tmp.value; });
    }
    };

7 泛型lambda表达式 C++14

C++14标准让lambda表达式具备了模版函数的能力,即泛型lambda表达式。泛型lambda表达式语法只需要使用auto占位符即可,例如

1
2
3
4
5
6
int main()
{
auto foo = [](auto a) { return a; };
int three = foo(3);
char const* hello = foo("hello");
}

8 常量lambda表达式和捕获*this C++17

C++17标准对lambda表达式同样有两处增强

  • 常量lambda表达式

  • 对捕获*this的增强。

    [*this] 的语法让程序生成了一个*this对象的副本并存储在lambda表达式内,可以在lambda表达式内直接访问这个复制对象的成员,消除了之前lambda表达式需要通过tmp访问对象成员的尴尬。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Work
    {
    private:
    int value;

    public:
    Work() : value(42) {}
    std::future<int> spawn()
    {
    return std::async([=, *this]() -> int { return value; });
    }
    };

9 捕获[=, this] C++20

[=] 可以捕获this指针,相似的,[=,*this] 会捕获this对象的副本。但是在代码中大量出现[=]和 [=,*this] 的时候我们可能很容易忘记前者与后者的区别。为了解决这个问题,在C++20标准中引入了[=, this]捕获this指针的语法,它实际上表达的意思和[=]相同,目的是让程序员们区分它与 [=,*this] 的不同

在C++20标准中还特别强调了要用[=, this]代替[=]

同时用两种语法捕获this指针是不允许的,比如:

1
[this, *this]{};

10 模板语法的泛型lambda表达式 C++20

C++20中添加模板对lambda的支持,语法非常简单:

1
2
3
4
5
6
7
8
9
10
[]<typename T>(T t) {}

auto f = []<typename T>(std::vector<T> vector) {
// …
};

auto f = []<typename T>(T const& x) {
T copy = x;
using Iterator = typename T::iterator;
};

11 可构造和可赋值的无状态lambda表达式 C++20

在C++20标准之前无状态的lambda表达式类型既不能构造也无法赋值,这阻碍了许多应用的实现。

举例来说,我们已经了解了像std::sort和std::find_if这样的函数需要一个函数对象或函数指针来辅助排序和查找,这种情况我们可以使用lambda表达式完成任务。但是如果遇到std::map这种容器类型就不好办了,因为std::map的比较函数对象是通过模板参数确定的,这个时候我们需要的是一个类型

1
2
3
4
5
6
auto greater = [](auto x, auto y) { return x > y; };
std::map<std::string, int, decltype(greater)> mymap;

auto greater = [](auto x, auto y) { return x > y; };
std::map<std::string, int, decltype(greater)> mymap1, mymap2;
mymap1 = mymap2;

上述代码在C++17中不可行,因为lambda表达式类型无法构造也无法赋值。

在C++17中要想让上述代码通过编译,需要明确的将greater对象传递给mymap

1
2
auto greater = [](auto x, auto y) { return x > y; };
std::map<std::string, int, decltype(greater)> mymap(greater);

C++20标准允许了无状态lambda表达式类型的构造和赋值,所以使用C++20标准的编译环境来编译上面的代码是可行的。


[C++]现代C++语言核心特性解析 Chapter 7
https://erlsrnby04.github.io/2024/10/31/C-现代C-语言核心特性解析-Chapter-7/
作者
ErlsrnBy04
发布于
2024年10月31日
许可协议