1.8、捕获*this的按值捕获
什么是C++17的按值捕获*this?
传统上,lambda表达式捕获类成员时,捕获的是this
指针,也就是捕获对象的地址。这样做意味着lambda内部访问的是原对象的成员,实际上是通过指针间接访问:
auto lambda = [this] { return member_var; };
这种捕获是按引用捕获this
指针,lambda闭包内存储的是指针,不是对象本身的拷贝。问题是,如果lambda的生命周期超过了原对象,this
指针就悬空,访问会导致未定义行为。
C++17引入了[*this]
语法,允许lambda捕获当前对象的一个拷贝(按值捕获对象),相当于把整个对象复制一份存到闭包里:
auto lambda = [*this] { return member_var; };
这意味着lambda内部访问的是对象的副本,避免了悬空指针的风险,尤其适合异步、并发场景或对象即将销毁但lambda还需使用其状态的情况。
底层原理和设计哲学
底层原理
- • 捕获this指针:lambda闭包内部存储的是指针,调用时通过指针访问成员。
- • 捕获*this(C++17):lambda闭包内部存储的是对象的拷贝,调用时访问的是闭包内的副本成员。
从编译器角度看,[*this]
等价于C++14的初始化捕获:
auto lambda = [self = *this] { return self.member_var; };
但C++17提供了更简洁的语法,避免了显式命名副本的繁琐。
设计哲学
- • 安全优先:避免悬空指针和未定义行为,尤其在异步和多线程环境中。
- • 语义清晰:
[*this]
直观表达“捕获对象的值”,比[this]
更明确。 - • 兼容性和简洁性:保持与C++14初始化捕获兼容,语法更简洁,易读易写。
- • 性能权衡:按值捕获意味着拷贝对象,适合小型或可拷贝对象;大型对象需谨慎,避免不必要的性能开销。
深度案例讲解
1. 基础示例:捕获*this的安全性
#include <iostream>
#include <string>
struct Person {
std::string name;
int age;
auto getLambdaByThis() {
// 捕获this指针,访问成员
return [this] { return name + " is " + std::to_string(age) + " years old."; };
}
auto getLambdaByValue() {
// 捕获*this的副本
return [*this] { return name + " is " + std::to_string(age) + " years old."; };
}
};
int main() {
auto lambda1 = Person{"Alice", 30}.getLambdaByThis();
auto lambda2 = Person{"Bob", 25}.getLambdaByValue();
// Person临时对象已经销毁
// lambda1调用会导致悬空指针访问,未定义行为
// lambda2调用安全,使用的是拷贝对象
std::cout << lambda2() << std::endl; // 输出:Bob is 25 years old
// lambda1()调用风险大,可能崩溃或输出垃圾
}
分析:
- •
getLambdaByThis()
返回的lambda捕获的是this
指针,指向临时对象,临时对象销毁后指针悬空。 - •
getLambdaByValue()
返回的lambda捕获的是对象副本,lambda内部拥有自己的数据拷贝,安全可靠。
2. 进阶示例:修改捕获对象副本
#include <iostream>
struct Counter {
int count = 0;
auto makeIncrementer() {
// 捕获*this按值,闭包内有一份副本
return [*this]() mutable {
count++; // 修改的是副本的count,不影响原对象
std::cout << "Inside lambda count: " << count << std::endl;
};
}
};
int main() {
Counter c;
auto inc = c.makeIncrementer();
inc(); // 1
inc(); // 2
std::cout << "Original count: " << c.count << std::endl; // 0,原对象未变
}
分析:
- • 捕获
*this
产生对象副本,lambda内对count
的修改是修改副本,不影响原对象。 - • 需要
mutable
修饰lambda,否则无法修改捕获的副本成员。 - • 这种用法适合需要在lambda内独立维护状态的场景。
3. 高级用法:结合其他捕获和异步场景
#include <iostream>
#include <thread>
#include <chrono>
struct Data {
int value = 42;
auto getAsyncLambda() {
// 捕获*this按值,保证lambda内数据安全
return [*this]() {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::cout << "Value: " << value << std::endl;
};
}
};
int main() {
Data d;
auto lambda = d.getAsyncLambda();
std::thread t(lambda);
t.join();
// 即使d对象销毁,lambda内的副本依然有效
}
分析:
- • 异步执行时,捕获
this
指针风险极大,因为原对象可能已销毁。 - • 捕获
*this
按值拷贝对象,lambda内数据独立,线程安全。 - • 这是C++17设计此特性的初衷之一。
常见错误使用及陷阱
- 1. 误以为捕获*this是捕获指针:
[*this]
捕获的是对象副本,不是指针,修改副本不影响原对象。 - 2. 捕获大型对象导致性能问题:捕获
*this
会调用拷贝构造,若对象很大或拷贝代价高,可能影响性能。 - 3. 忘记给lambda加mutable:捕获副本后,若要修改成员变量,必须加
mutable
,否则编译报错。 - 4. 混用捕获模式导致编译错误:捕获列表中不能同时出现
this
和*this
,且捕获同名变量会报错。 - 5. 忽略对象成员的深拷贝需求:如果成员包含指针或资源,按值捕获只做浅拷贝,可能需要自定义拷贝行为。
面试中可能出现的问题
- 1. 解释捕获
this
和捕获*this
的区别及使用场景。 - 2. 如何避免lambda捕获
this
导致的悬空指针问题? - 3. 为什么捕获
*this
需要mutable
? - 4. 捕获
*this
时,lambda闭包的大小如何变化? - 5. 在多线程环境中,如何安全使用lambda捕获类成员?
- 6. C++14和C++17中捕获对象副本的写法区别?
总结
C++17的按值捕获*this
是对lambda捕获机制的重大改进,既解决了传统捕获this
指针的安全隐患,又提供了更简洁的语法。它体现了现代C++设计哲学中对安全、简洁和性能的平衡。掌握它不仅能写出更健壮的代码,还能在多线程、异步编程中避免难以发现的bug。
本文首发于【讳疾忌医-note】公众号,未经授权,不得转载。
(加入我的知识星球,免费获取账号,解锁所有文章。)
阅读剩余
版权声明:
作者:讳疾忌医-note
链接:https://www.1217zy.vip/archives/1037
文章版权归作者所有,未经允许请勿转载。
THE END