C++C++17constexpr Lambda 和 捕获 *this 的拷贝
苏丙榅1. C++17 的主要改进
Lambda 表达式是 C++11 引入的一种匿名函数(没有名字的函数)。它可以让我们在需要函数的地方快速定义一个小型函数,特别适合用于算法、回调函数等场景。
C++17 对 Lambda 表达式进行了改进,让我们用起来更方便!
2. constexpr Lambda
在 C++17 之前,Lambda 表达式默认是不能在编译期常量表达式(如 constexpr 函数或模板参数)中使用的。虽然编译器经常在内部做一些优化,但标准层面并不允许显式地将 Lambda 用于编译期计算。
C++17 规定,Lambda 表达式默认就是隐式的 constexpr(前提是它的函数体满足 constexpr 的要求)。这意味着我们可以在编译期调用 Lambda,比如在 constexpr 函数里使用 Lambda、把 Lambda 当作模板参数传递。这极大地增强了 C++ 的元编程能力。
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 41 42 43 44 45 46 47 48 49 50
| #include <iostream> #include <type_traits> #include <array>
constexpr auto getSquareLambda() { return [](int n) { return n * n; }; }
int main() { constexpr auto is_even = [](int n) { return n % 2 == 0; }; static_assert(is_even(4), "4应该是偶数"); static_assert(!is_even(5), "5应该是奇数"); std::cout << "静态断言测试通过!" << std::endl; constexpr auto add = [](int a, int b) { return a + b; }; std::array<int, add(10, 5)> arr; std::cout << "数组大小: " << arr.size() << std::endl; constexpr int value = 42; if constexpr ([](int n) { return n > 0; }(value)) { std::cout << "value是正数" << std::endl; } constexpr auto square = getSquareLambda(); constexpr int result = square(5); std::cout << "5 的平方是 (编译期计算): " << result << std::endl;
int x = 10; std::cout << "10 的平方是 (运行期计算): " << square(x) << std::endl; return 0; }
|
在 C++11/14 中,Lambda 默认是 const 的。也就是说,如果你没有在参数列表后写 mutable,你就不能修改按值捕获的变量。
但是,C++17 允许 Lambda 是 constexpr 的。这引出了一个有趣的问题:如果我给 Lambda 加上 constexpr 修饰符(虽然通常不需要显式加),它还能修改捕获的变量吗?
C++17 规定,如果 Lambda 满足 constexpr 的条件,它依然遵循 Lambda 的基本规则:默认是不可变。
- 如果尝试修改按值捕获的变量,必须在参数列表后加上
mutable 关键字。
- 一旦加上了
mutable,这个 Lambda 就不再是 constexpr(因为它改变了状态)。
这个改进主要是理清了规则:Lambda 可以是编译期常量,但前提是它像 const 函数一样不修改内部状态。
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 base = 10;
auto mutableFunc = [base]() mutable { base++; return base; }; std::cout << "mutable call: " << mutableFunc() << std::endl;
constexpr int number = 9; auto constexprFunc = [number](int n) { return number + n; };
constexpr int compileTimeVal = constexprFunc(5); std::cout << "constexpr call: " << compileTimeVal << std::endl;
return 0; }
|
自定义的lambda表达式如果捕获了外部变量,变量的属性会影响到该Lambda的属性,比如:
1 2 3 4
| constexpr int number = 9; auto constexprFunc = [number](int n) { return number + n; };
|
- 如果捕获的是编译期常量,这个
Lambda是constexpr
- 如果捕获的是运行时变量,并在函数体内使用,这个
Lambda不是constexpr
- 如果捕获的是运行时变量,但在函数体中没有使用它,
Lambda 仍可以是 constexpr
- 如果捕获的是编译期常量,但是函数体包含非法操作(如
new、throw),也不是 constexpr
3. 捕获 *this 的拷贝
C++17 引入了 [*this] 捕获方式。它不是捕获指针,而是把当前对象(整个实体)复制一份传给 Lambda。这样做的好处是 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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| #include <iostream> #include <string> #include <thread> #include <functional>
class Worker { public: Worker(std::string name) : m_name(name) {}
std::function<void()> getTaskOldStyle() { return [this]() { std::cout << "[Old Style] My name is: " << this->m_name << std::endl; }; }
std::function<void()> getTaskNewStyle() { return [*this]() { std::cout << "[New Style] My name is: " << m_name << std::endl; }; }
private: std::string m_name; };
int main() { { auto task = Worker("Alice").getTaskNewStyle(); task(); }
{ auto task1 = Worker("Bob").getTaskOldStyle(); Worker worker2("Hello, world"); task1(); }
return 0; }
|
程序运行的结果:
1 2
| [New Style] My name is: Alice [Old Style] My name is: Hello, world
|
- 在
getTaskNewStyle 中,我们使用了 [*this]。这意味着 Lambda 内部保存了一个 Worker 对象的副本。
- 即使我们在
main 函数中创建的是一个临时对象 Worker("Alice") 并取出了 task,临时对象随即销毁,task 中的副本依然存在,调用时不会出错。
为什么这里看起来能”正常工作” ?
1 2 3 4
| { auto task1 = Worker("Bob").getTaskOldStyle(); task1(); }
|
Worker("Bob") 创建一个临时(匿名)对象
- 调用
getTaskOldStyle() 方法,返回一个 Lambda,Lambda 捕获了 this 指针(指向临时对象)
- 表达式结束后,临时对象立即被销毁,将返回的 Lambda 赋值给
task1
- 调用
task1(); 相当于访问了已销毁的对象,为什么程序可以正常执行有一些原因:
- 临时对象是在栈上创建的,内存未被立即覆盖
- 重新重新创建新的临时对象,会覆盖原来的栈内存,最后输出
Hello, world