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; }
|
当 lambda 捕获列表为空 、函数体无运行时操作时,满足 C++17 隐式 constexpr 的条件,如果自定义的lambda表达式捕获了外部变量,这样会影响到该Lambda的属性,比如:
- 如果捕获的是编译期常量,并在函数体内使用,这个
Lambda不是constexpr
- 如果捕获的是编译期常量,但在函数体中没有使用它,
Lambda 仍是 constexpr
- 如果捕获的是运行时变量,并在函数体内使用,这个
Lambda不是constexpr
- 如果捕获的是运行时变量,但在函数体中没有使用它,
Lambda 仍是 constexpr
- 如果没有捕获外部变量,但是函数体包含非法操作(如
IO 操作、static 局部变量、try/catch、new等 ),也不是 constexpr
在上述描述中,对于在Lambda捕获列表中捕获了外部变量/constexpr 常量但是没有使用的这种情况的说明:主流编译器(GCC、Clang 部分版本)会放宽检查,将捕获的未访问成员消除,这样就等价于没有捕获外部变量,优化后变成无捕获 lambda,这样就能满足隐式 constexpr的要求。
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 52 53
| #include <iostream>
int main() { auto add1 = [base](int a) { return a; }; if constexpr (add1(4)) { cout << "add1 ok..." << endl; }
auto add2 = [base](int a) { return a + base; }; if constexpr (add2(4)) { cout << "add2 ok..." << endl; }
constexpr int count = 9; auto add3 = [count](int a) { return a ; }; if constexpr (add3(9)) { cout << "ok..." << endl; }
auto add4 = [count](int a) { return a + count ; }; if constexpr (add4(9)) { cout << "ok..." << endl; } auto bad1 = [](int a) { static int x = 0; return a + x++; }; static_assert(bad1(12));
return 0; }
|
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