C++C++14Lambda 表达式的优化
苏丙榅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, ...) { };
auto lambda1 = [](int x, int y) { return x + y; };
auto lambda2 = [](auto x, auto y) { return x + y; }; auto lambda3 = [](auto&& x) { return x * 2; };
|
- 每个
auto 参数都是独立的类型参数,也就是说param1,param2可以是不同的类型
- 例如:
lambda2(5, 3.14) 中 x 是 int,y 是 double
我们可以试着探索一下它的底层实现机制,以编译器视角来看的话当写下这样的泛型 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() { auto add = [](auto a, auto b) { return a + b; }; std::cout << add(1, 2) << std::endl; std::cout << add(1.5, 2.5) << std::endl; std::cout << add(std::string("Hello "), std::string("World")) << std::endl;
auto compare = [](auto a, auto b) -> bool { return a == b; }; std::cout << compare(10, 10) << std::endl; std::cout << compare(10, 20) << std::endl; std::cout << compare("abc", "abc") << std::endl; return 0; }
|
1.2.2 泛型查找
在示例代码中,提供的泛型查找函数可以为容器std::vector、std::list、std::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:
1.2.4 泛型函数组合器
泛型函数组合器是指能够接受函数作为参数、返回新函数的高阶函数,并且使用泛型来保持类型安全。它们在函数式编程中非常常见,用于构建、转换或组合函数。其核心特征如下:
- 高阶函数:接收函数作为参数或返回函数
- 泛型类型:使用类型参数(如
<T, R>)来保持类型安全
- 组合能力:将多个函数组合成新的功能
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>
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; std::cout << squareThenToString(4) << std::endl; std::cout << incrementThenSquare(3.5) << std::endl; return 0; }
|
上述代码中关于泛型函数组合器的使用的推导过程如下:
1 2 3
| auto increment = [](auto x) { return x + 1; }; auto square = [](auto x) { return x * x; }; auto incrementThenSquare = compose(square, increment);
|
compose(square, increment) 被调用
- 通过表达式进行类型推导
F = decltype(square),G = decltype(increment)
- 返回
__compose_lambda<F, G> 类型的对象,这个对象捕获了 square 和 increment
然后再调用:incrementThenSquare(5);这行代码,这相当于:
increment(5) → 6
square(increment(5)) → 36
2. 捕获变量的增强
C++14 引入了 初始化捕获(也称为 广义 lambda 捕获),允许在 lambda 捕获列表中直接初始化捕获的变量。这是在 C++11 基础上对 lambda 功能的重大扩展。基本语法如下:
1 2
| [捕获变量 = 初始化表达式](参数列表) -> 返回类型 { 函数体 }
|
2.1 捕获只读副本
2.1.1 值捕获(默认只读)
1 2 3 4 5 6 7 8 9 10 11
| int main() { int x = 10; auto lambda = [y = x + 5]() { return y * 2; }; std::cout << lambda() << std::endl; }
|
[y = x + 5] 是在 lambda 对象的内部创建了一个新的成员变量 y,并用表达式 x + 5 的结果(即 15)来初始化它。其中有几点需要大家注意:
int y不是在函数作用域中定义的局部变量,它是在lambda 的闭包类型 中创建了一个数据成员 y
- 即使外部变量
x 后续改变,y 的值也不会改变(因为它是拷贝初始化的)
- 这个数据成员的初始化在 lambda 被创建时执行,并且该数据成员的生命周期与 lambda 对象相同
可以把它想象成类似于这样的结构(概念上):
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class UnnamedLambda { private: int y; public: UnnamedLambda(int x) : y(x + 5) {} int operator()() const { return y * 2; } };
auto lambda = UnnamedLambda(x);
|
关于运算符重载函数int operator()() const{}尾部const修饰的含义大家一定要掌握:
- 该函数不会修改对象的状态
- 在函数内部,所有成员变量都是只读的(const)
- 函数体内部只能调用其他 const 成员函数
下面代码是在C++11和C++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]() { std::cout << "x = " << x << std::endl; return x * 2; }; lambda1(); auto lambda2 = [y = x]() { std::cout << "y = " << y << std::endl; 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]() { std::cout << "String: " << s << std::endl; std::cout << "Vector size: " << v.size() << std::endl; std::cout << "First element: " << v[0] << std::endl; }; lambda(); str = "Modified"; vec.push_back(4); lambda(); 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; 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; auto lambda1 = [&x]() { x = 30; return x; }; auto lambda2 = [&y = x]() { y = 40; return y; }; std::cout << "Initial x: " << x << std::endl; lambda1(); std::cout << "After lambda1: x = " << x << std::endl; lambda2(); std::cout << "After lambda2: x = " << x << std::endl; std::cout << "Final x: " << x << std::endl; return 0; }
|
[&y = x]:创建一个名为 y 的引用,它引用外部变量 x
y 是 x 的别名(另一个名字),对 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; auto lambda = [y = std::ref(x)]() { y.get() = 30; return y.get() * 2; }; std::cout << "Before: " << x << std::endl; lambda(); std::cout << "After: " << x << std::endl; 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; }; auto lambda2 = [cref = std::cref(y)]() { return cref.get(); }; std::cout << lambda1() << std::endl; std::cout << lambda2() << std::endl; 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; return [&y = value]() { return y; }; }
int main() { auto lambda = create_lambda(); return 0; }
|
2.3 移动捕获
在 C++11 中,lambda 捕获只能按值 [=]、按引用 [&] 或显式指定变量名,但无法直接进行移动捕获。
C++14 引入了广义捕获,允许在捕获列表中直接移动捕获只能移动的类型(如 unique_ptr):
1 2 3
| auto lambda = [var = std::move(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 { } };
|
重要注意事项:
移动后的状态:外部的var在移动后不应再使用(除非重新赋值)
const限定:如果需要修改捕获的变量,需要添加mutable关键字
1 2 3
| auto lambda = [var = std::move(var)]() mutable { };
|
生命周期: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)); auto lambda = [p = std::move(ptr)]() { std::cout << "Value: " << *p << std::endl; *p = 100; std::cout << "Modified to: " << *p << std::endl; }; std::cout << "ptr is " << (ptr ? "not null" : "null") << std::endl; lambda(); }
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}; auto lambda = [v = std::move(numbers)]() mutable { std::cout << "Original: "; for (int n : v) std::cout << n << " "; std::cout << std::endl; 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() 等)
基于上面的示例代码我们需要掌握以下几个关键知识点:
- const 性质:默认情况下,lambda 的
operator() 是 const 成员函数,所以所有按值捕获的变量都是 const 的
mutable 关键字:允许在 lambda 内修改按值捕获的变量
- 移动语义:
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; }
|
关于模板函数 auto createLambda(T&& value){...}
T&& 是通用引用(如果 T 被推导,则为转发引用)
std::forward<T>(value) 实现完美转发
captured = std::forward<T>(value) 是 C++14 的通用 lambda 捕获语法
关于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
|
关于 mutable 的使用:
Lambda 的调用运算符是 const 的
移动语义问题