Lambda 表达式的优化

1. 泛型 Lambda

1.1 语法

C++14 引入了泛型 Lambda,允许 Lambda 表达式的参数使用 auto 类型推导。这使得编译器能够为每个调用点自动推导参数类型,本质上编译器会生成一个函数对象模板

1
2
3
4
5
6
7
8
9
10
11
// 基本语法
auto lambda = [](auto param1, auto param2, ...) {
// 函数体
};
// 语法对比
// C++11: 显式指定参数类型
auto lambda1 = [](int x, int y) { return x + y; };

// C++14: 使用 auto 推导参数类型
auto lambda2 = [](auto x, auto y) { return x + y; };
auto lambda3 = [](auto&& x) { return x * 2; };
  • 每个 auto 参数都是独立的类型参数,也就是说param1param2可以是不同的类型
  • 例如:lambda2(5, 3.14)xintydouble

我们可以试着探索一下它的底层实现机制,以编译器视角来看的话当写下这样的泛型 lambda:

1
2
int a = 5, b = 10;
auto lambda = [a, b](int x, int y) { return a*x + b*y; };

编译器大致会将其转换为(以下展示的为伪代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class __lambda_1 
{
private:
int __a; // 值捕获的变量
int __b; // 值捕获的变量

public:
// 构造函数:初始化捕获的变量
__lambda_1(int a_captured, int b_captured)
: __a(a_captured), __b(b_captured) {}

// 调用运算符
auto operator()(int x, int y) const
{
return __a * x + __b * y;
}
};

1.2 使用示例

1.2.1 泛型加法和比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <string>
#include <vector>

int main()
{
// 泛型加法 Lambda
auto add = [](auto a, auto b) {
return a + b;
};

std::cout << add(1, 2) << std::endl; // int: 3
std::cout << add(1.5, 2.5) << std::endl; // double: 4.0
// string: Hello World
std::cout << add(std::string("Hello "), std::string("World")) << std::endl;

// 带模板的泛型 Lambda
auto compare = [](auto a, auto b) -> bool {
return a == b;
};

std::cout << compare(10, 10) << std::endl; // true
std::cout << compare(10, 20) << std::endl; // false
std::cout << compare("abc", "abc") << std::endl; // true(比较指针)

return 0;
}

1.2.2 泛型查找

在示例代码中,提供的泛型查找函数可以为容器std::vectorstd::liststd::array提供元素搜索功能,结果返回true或者false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
#include <vector>
#include <list>
#include <array>
#include <algorithm>

// 泛型查找函数
auto findIfContains = [](const auto& container, const auto& value)
{
return std::find(container.begin(), container.end(), value) != container.end();
};

int main()
{
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::list<std::string> words = {"apple", "banana", "cherry"};
std::array<int, 3> arr = {10, 20, 30};

// 查找不同类型容器中的元素
bool found1 = findIfContains(numbers, 3);
bool found2 = findIfContains(words, "banana");
bool found3 = findIfContains(arr, 20);

return 0;
}

1.2.3 泛型排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <vector>
#include <string>
#include <algorithm>

// 泛型降序排序
auto descending = [](const auto& a, const auto& b)
{
return a > b; // 要求类型支持 > 操作符
};

// 泛型按照成员排序
auto sortByMember = [](auto memberPtr)
{
return [memberPtr](const auto& a, const auto& b)
{
return a.*memberPtr < b.*memberPtr;
};
};

struct Person
{
std::string name;
int age;
};

int main()
{
// 对不同类型的容器排序
std::vector<int> ints = {5, 2, 8, 1, 9};
std::vector<std::string> strs = {"zebra", "apple", "banana"};

std::sort(ints.begin(), ints.end(), descending);
std::sort(strs.begin(), strs.end(), descending);

// 按成员排序
std::vector<Person> people = {{"Alice", 30}, {"Bob", 25}, {"Charlie", 35}};
std::sort(people.begin(), people.end(), sortByMember(&Person::age));

return 0;
}

关于上面代码中sortByMember函数一个高阶函数工厂,它生成一个比较函数(用于排序)。我们来逐层解释这两个 return

  • 第一个 return(外层 lambda 的 return)

    1
    return [memberPtr](const auto& a, const auto& b) { ... };
    • 外层 lambda 创建时 接收成员指针,但这个成员指针需要保存下来,供后面的比较使用

      1
      2
      3
      4
      5
      6
      7
      8
      9
      // ❌ 错误示例:不捕获成员指针
      auto sortByMemberWrong = [](auto memberPtr) {
      // 错误:memberPtr 是参数,只能在当前作用域使用
      return [](const auto& a, const auto& b) {
      // 这里无法访问 memberPtr!
      // return a.*memberPtr < b.*memberPtr; // 编译错误
      return false;
      };
      };
    • 捕获就是为了跨 lambda 生命周期传递数据,返回一个新的lambda函数(闭包)

      闭包是一个函数和其关联的环境(捕获的变量)的组合。它让函数可以”记住”并访问创建时的上下文,即使这个上下文已经不在作用域内。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      #include <iostream>
      int main()
      {
      int x = 10; // 外部变量
      // Lambda 表达式创建闭包
      auto closure = [x](int y) {
      // 可以访问捕获的变量x
      return x + y;
      };

      // 即使离开x的作用域...
      {
      int x = 100; // 新的x,不会影响闭包
      std::cout << closure(5) << std::endl; // 输出 15,不是 105
      }

      // 闭包记住了创建时的x=10
      std::cout << closure(20) << std::endl; // 输出 30

      return 0;
      }
    • 捕获:通过值捕获外层参数 memberPtr(成员指针)

    • 效果:工厂函数创建了一个定制化的比较器

  • 第二个 return(内层 lambda 的 return)

    1
    return a.*memberPtr < b.*memberPtr;
    • 作用:实现实际的比较逻辑
    • a.*memberPtr 的所有组成部分剖析:
      • a:对象实例(可以是指针或引用)
      • .*:成员指针解引用运算符
      • memberPtr:成员指针
    • 操作:比较这两个成员的值
      • a.*memberPtr:通过成员指针访问对象a的特定成员
      • b.*memberPtr:通过成员指针访问对象b的相同成员

1.2.4 泛型函数组合器

泛型函数组合器是指能够接受函数作为参数、返回新函数的高阶函数,并且使用泛型来保持类型安全。它们在函数式编程中非常常见,用于构建、转换或组合函数。其核心特征如下:

  1. 高阶函数:接收函数作为参数或返回函数
  2. 泛型类型:使用类型参数(如 <T, R>)来保持类型安全
  3. 组合能力:将多个函数组合成新的功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <string>

// 泛型函数组合:f(g(x))
auto compose = [](auto f, auto g) {
return [f, g](auto x) {
return f(g(x));
};
};

// 使用示例
auto increment = [](auto x) { return x + 1; };
auto square = [](auto x) { return x * x; };
auto toString = [](auto x) { return std::to_string(x); };

auto incrementThenSquare = compose(square, increment);
auto squareThenToString = compose(toString, square);

int main()
{
std::cout << incrementThenSquare(5) << std::endl; // (5+1)^2 = 36
std::cout << squareThenToString(4) << std::endl; // "16"

// 也适用于浮点数
std::cout << incrementThenSquare(3.5) << std::endl; // 20.25

return 0;
}

上述代码中关于泛型函数组合器的使用的推导过程如下:

1
2
3
auto increment = [](auto x) { return x + 1; };
auto square = [](auto x) { return x * x; };
auto incrementThenSquare = compose(square, increment);
  1. compose(square, increment) 被调用
  2. 通过表达式进行类型推导 F = decltype(square)G = decltype(increment)
  3. 返回 __compose_lambda<F, G> 类型的对象,这个对象捕获了 squareincrement

然后再调用:incrementThenSquare(5);这行代码,这相当于:

  1. increment(5) → 6
  2. square(increment(5)) → 36

2. 捕获变量的增强

C++14 引入了 初始化捕获(也称为 广义 lambda 捕获),允许在 lambda 捕获列表中直接初始化捕获的变量。这是在 C++11 基础上对 lambda 功能的重大扩展。基本语法如下:

1
2
// C++14 初始化捕获语法
[捕获变量 = 初始化表达式](参数列表) -> 返回类型 { 函数体 }

2.1 捕获只读副本

2.1.1 值捕获(默认只读)

1
2
3
4
5
6
7
8
9
10
11
int main() 
{
int x = 10;

// C++14: 用 x+5 初始化 y
auto lambda = [y = x + 5]() {
return y * 2;
};

std::cout << lambda() << std::endl; // 输出 30
}

[y = x + 5] 是在 lambda 对象的内部创建了一个新的成员变量 y,并用表达式 x + 5 的结果(即 15)来初始化它。其中有几点需要大家注意:

  1. int y不是在函数作用域中定义的局部变量,它是在lambda 的闭包类型 中创建了一个数据成员 y
  2. 即使外部变量 x 后续改变y 的值也不会改变(因为它是拷贝初始化的)
  3. 这个数据成员的初始化在 lambda 被创建时执行,并且该数据成员的生命周期与 lambda 对象相同

可以把它想象成类似于这样的结构(概念上):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class UnnamedLambda 
{
private:
int y; // lambda 内部的数据成员
public:
UnnamedLambda(int x) : y(x + 5) {} // 用 x+5 初始化

int operator()() const {
return y * 2;
}
};

// 使用时:
auto lambda = UnnamedLambda(x);

关于运算符重载函数int operator()() const{}尾部const修饰的含义大家一定要掌握:

  • 该函数不会修改对象的状态
  • 在函数内部,所有成员变量都是只读的(const)
  • 函数体内部只能调用其他 const 成员函数

下面代码是在C++11C++14中,使用Lambda表达式进行值捕获的两种书写形式对比:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>

int main()
{
int x = 10;

// 简单值捕获 - 创建只读副本
auto lambda1 = [x]() {
// x 是 const int,因为 lambda 默认 const
std::cout << "x = " << x << std::endl; // OK
// x = 20; // 错误:不能修改 const 对象
return x * 2;
};

lambda1();

// C++14 广义 lambda 捕获(更好的方式)
auto lambda2 = [y = x]() {
// y 是 x 的副本,也是 const 的
std::cout << "y = " << y << std::endl;
// y = 30; // 错误:不能修改 const 对象
return y;
};

lambda2();

return 0;
}

2.1.2 复杂对象的值捕获

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
#include <string>
#include <vector>

int main()
{
std::string str = "Hello";
std::vector<int> vec = {1, 2, 3};

// 捕获多个只读副本
auto lambda = [s = str, v = vec]() {
// s 和 v 都是只读副本
std::cout << "String: " << s << std::endl;
std::cout << "Vector size: " << v.size() << std::endl;
std::cout << "First element: " << v[0] << std::endl;

// 以下修改都会编译错误:
// s.append(" World");
// v.push_back(4);
// v[0] = 99;
};

lambda();

// 原始对象可以修改,不影响 lambda 中的副本
str = "Modified";
vec.push_back(4);

lambda(); // 仍然输出 "Hello" 和 size 3

return 0;
}

2.1.3 计算表达式结果的只读副本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <cmath>

int main()
{
int base = 2;
int exponent = 8;

// 捕获计算结果的只读副本
auto lambda = [result = static_cast<int>(pow(base, exponent))]() {
std::cout << "2^8 = " << result << std::endl;
// result = 300; // 错误:只读
return result;
};

std::cout << "Result: " << lambda() << std::endl;

return 0;
}

2.2 引用初始化捕获

在 C++14 中,广义 lambda 捕获也可以用于引用捕获

2.2.1 基本的引用捕获

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>

int main()
{
int x = 10;

// 传统引用捕获(C++11)
auto lambda1 = [&x]() {
x = 30;
return x;
};

// 广义引用捕获(C++14)
auto lambda2 = [&y = x]() {
y = 40;
return y;
};

std::cout << "Initial x: " << x << std::endl; // 10

lambda1();
std::cout << "After lambda1: x = " << x << std::endl; // 30

lambda2();
std::cout << "After lambda2: x = " << x << std::endl; // 40

// y 只是 lambda2 内部的名字,外部仍然用 x
std::cout << "Final x: " << x << std::endl; // 40

return 0;
}
  • [&y = x]:创建一个名为 y 的引用,它引用外部变量 x
  • yx 的别名(另一个名字),对 y 的任何操作都会直接作用在 x

2.2.2 使用 std::ref 捕获引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <functional>
#include <iostream>

int main()
{
int x = 10;

// 创建 x 的引用包装器
auto lambda = [y = std::ref(x)]() {
y.get() = 30; // 通过 get() 访问原始引用
return y.get() * 2;
};

std::cout << "Before: " << x << std::endl; // 10
lambda();
std::cout << "After: " << x << std::endl; // 30

return 0;
}

std::ref(x) 创建一个对变量 x引用包装器

  • 默认情况下,lambda通过值捕获时会复制变量,使用 std::ref 可以捕获引用而不复制值
  • y.get() 获取包装的原始引用

2.2.3 常量引用捕获

如果要捕获的数据是常量,在进行引用捕获的时候可以使用static_cast、或者std::cref

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <functional>
#include <iostream>

int main()
{
const int x = 10;
int y = 20;

// 常量引用捕获
auto lambda1 = [&ref = static_cast<const int&>(y)]() {
return ref; // 可以读取,但不能修改
};

// 或者使用 std::cref
auto lambda2 = [cref = std::cref(y)]() {
return cref.get();
};

std::cout << lambda1() << std::endl; // 20
std::cout << lambda2() << std::endl; // 20

return 0;
}
  • std::cref 更现代、意图更明确,适合大多数场景
  • static_cast 更直接,适合需要精确控制引用类型的情况
  • 两者在性能和功能上几乎相同,主要区别在于代码风格和可读性

2.2.4 引用捕获临时对象(危险!)

在使用引用初始化捕获的时候,需要特别注意生命周期的问题,如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <functional>

std::function<int()> create_lambda()
{
int value = 100;
// 危险!value 是局部变量,函数返回后会被销毁
return [&y = value]() {
return y; // 返回时,y 引用的是已销毁的对象!
};
}

int main()
{
auto lambda = create_lambda();
// 未定义行为!value 已被销毁
// std::cout << lambda() << std::endl;

return 0;
}

2.3 移动捕获

在 C++11 中,lambda 捕获只能按值 [=]、按引用 [&] 或显式指定变量名,但无法直接进行移动捕获。

C++14 引入了广义捕获,允许在捕获列表中直接移动捕获只能移动的类型(如 unique_ptr):

1
2
3
auto lambda = [var = std::move(var)]() {
// 使用 var(已被移动)
};
  • 移动语义:将外部变量var的内容移动到lambda内部的var
  • 转移所有权:外部var进入有效但未指定的状态(通常为空或默认状态)
  • 性能优化:避免不必要的拷贝,特别是对于大型对象或只移动类型(如std::unique_ptr
  • var = std::move(var) :对lambda外部的变量var进行移动构造

类似于手写一个函数对象类:

1
2
3
4
5
6
7
8
9
10
11
12
class Lambda 
{
private:
T var; // 通过移动构造初始化

public:
Lambda(T&& outer_var) : var(std::move(outer_var)) {}

auto operator()() const {
// 函数体
}
};

重要注意事项:

  1. 移动后的状态:外部的var在移动后不应再使用(除非重新赋值)

  2. const限定:如果需要修改捕获的变量,需要添加mutable关键字

    1
    2
    3
    auto lambda = [var = std::move(var)]() mutable {
    // 可以修改var
    };
  3. 生命周期:lambda对象拥有移动后的数据,生命周期独立于原始变量

1.3.1 移动只能移动的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
#include <memory>
#include <vector>

void unique_ptr_example()
{
std::unique_ptr<int> ptr(new int(42));

// C++11 方式:无法直接移动,只能按值或引用捕获
// auto lambda1 = [ptr = std::move(ptr)]() {}; // C++11 错误

// C++14:移动捕获
// 移动捕获 unique_ptr(唯一所有权)
auto lambda = [p = std::move(ptr)]() {
std::cout << "Value: " << *p << std::endl;
// 可以修改指向的内容(注意此处修改的不是指针本身的值,而是指针指向的内容的值)
*p = 100;
std::cout << "Modified to: " << *p << std::endl;
};

// ptr 现在为空
std::cout << "ptr is " << (ptr ? "not null" : "null") << std::endl;

lambda(); // 输出: Value: 42, Modified to: 100
// 此时原始 ptr 已为空
}

int main()
{
unique_ptr_example();
return 0;
}

1.3.2 移动其它类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <string>
#include <iostream>

void example_string()
{
std::string large_string = "这是一个很长的字符串...";

auto lambda = [str = std::move(large_string)]() {
std::cout << str << std::endl;
};

lambda();
std::cout << "原始字符串: \"" << large_string << "\"" << std::endl; // 输出为空
}

int main()
{
example_string();
return 0;
}

3. 关于 mutable 的使用

3.1 移动捕获 move

mutable 关键字在 C++14 lambda 表达式中有特殊用途,主要影响按值捕获的变量在 lambda 内部的可修改性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
#include <string>
#include <vector>

int main() {
std::vector<int> numbers = {1, 2, 3};

// 移动捕获 + mutable
auto lambda = [v = std::move(numbers)]() mutable {
std::cout << "Original: ";
for (int n : v) std::cout << n << " ";
std::cout << std::endl;

// 修改移动进来的 vector
v.push_back(4);

std::cout << "After push: ";
for (int n : v) std::cout << n << " ";
std::cout << std::endl;

return std::move(v); // 返回时再次移动
};

auto result = lambda();

std::cout << "Result: ";
for (int n : result) std::cout << n << " ";
std::cout << std::endl;

return 0;
}

**如果不在示例代码中添加 **mutable关键字

  • 默认情况下,Lambda 的 operator()const 成员函数,因此v 是 const 的(即使它是移动捕获的)
  • 不能修改 v 中的任何元素,也不能调用 v 的非 const 成员函数(如 push_back()pop_back()clear() 等)

基于上面的示例代码我们需要掌握以下几个关键知识点:

  1. const 性质:默认情况下,lambda 的 operator() 是 const 成员函数,所以所有按值捕获的变量都是 const 的
  2. mutable 关键字:允许在 lambda 内修改按值捕获的变量
  3. 移动语义:std::move(numbers) 只是将 numbers 的内容移动到 v 中,不改变 v 在 lambda 中的 const 性质

因此我们可以这样进行简单记忆:按值捕获 + 需要修改 = 必须加 mutable

3.2 完美转发

在 C++14 中,Lambda 的广义捕获允许我们在捕获列表中初始化捕获变量,这可以与 std::forward 完美结合,用来实现智能的资源管理。std::forward 的核心作用是**完美转发 **(在转发过程中保持参数原有的值类别)。

下面是 template <class T> T&& std::forward<T>(t); 函数工作的基础:

  • 当 T 为左值引用类型时,t 将被转换为T类型的左值
  • 当 T 不是左值引用类型时,t 将被转换为T类型的右值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>
#include <string>

template<typename T>
auto createLambda(T&& value)
{
// 使用完美转发进行移动捕获
return [captured = std::forward<T>(value)]() mutable {
std::cout << "Captured value: " << captured << std::endl;
captured += " processed";
std::cout << "After processing: " << captured << std::endl;
};
}

int main()
{
std::string lvalue = "Lvalue string";

// 传递左值 - 复制捕获
auto lambda1 = createLambda(lvalue);
std::cout << "Original lvalue: " << lvalue << std::endl;
lambda1();

std::cout << "\n---\n";

// 传递右值 - 移动捕获
auto lambda2 = createLambda(std::string("Rvalue string"));
lambda2();

// 移动左值
std::cout << "\n---\n";
auto lambda3 = createLambda(std::move(lvalue));
std::cout << "After move, lvalue: \"" << lvalue << "\"\n";
lambda3();

return 0;
}

  1. 关于模板函数 auto createLambda(T&& value){...}

    • T&& 是通用引用(如果 T 被推导,则为转发引用)
    • std::forward<T>(value) 实现完美转发
    • captured = std::forward<T>(value) 是 C++14 的通用 lambda 捕获语法
  2. 关于lambda函数的三种调用方式分析

    • 传递左值 - 复制捕获

      1
      2
      std::string lvalue = "Lvalue string";
      auto lambda1 = createLambda(lvalue); // 传递左值
      • T 推导为 std::string&
      • std::forward<T>(value) 返回左值引用
      • captured = value 调用拷贝构造函数
      • lvalue 保持不变
      1
      2
      3
      4
      // 输出结果
      Original lvalue: Lvalue string
      Captured value: Lvalue string
      After processing: Lvalue string processed
    • 传递右值 - 移动捕获

      1
      auto lambda2 = createLambda(std::string("Rvalue string"));
      • T 推导为 std::string
      • std::forward<T>(value) 返回右值引用
      • captured = std::move(value) 调用移动构造函数
      • 临时对象被移动到 lambda 中
      1
      2
      3
      // 输出结果
      Captured value: Rvalue string
      After processing: Rvalue string processed
    • 移动左值 - 显式移动

      1
      auto lambda3 = createLambda(std::move(lvalue));
      • std::move(lvalue) 将左值转换为右值
      • T 推导为 std::string
      • captured = std::move(value) 调用移动构造函数
      • lvalue 变为有效但未指定的状态
      1
      2
      3
      4
      // 输出结果
      After move, lvalue: "" // 字符串可能为空
      Captured value: Lvalue string
      After processing: Lvalue string processed
  3. 关于 mutable 的使用:

    • Lambda 的调用运算符是 const

      • 默认情况下,Lambda 的 operator()const 成员函数

      • 这意味着我们不能修改捕获的变量

    • 移动语义问题

      • std::forward<T>(value) 可能会进行移动构造

      • 如果在 Lambda 内部需要修改 captured(即使是移动它),需要使用 mutable