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. 1. 误以为捕获*this是捕获指针[*this]捕获的是对象副本,不是指针,修改副本不影响原对象。
  2. 2. 捕获大型对象导致性能问题:捕获*this会调用拷贝构造,若对象很大或拷贝代价高,可能影响性能。
  3. 3. 忘记给lambda加mutable:捕获副本后,若要修改成员变量,必须加mutable,否则编译报错。
  4. 4. 混用捕获模式导致编译错误:捕获列表中不能同时出现this*this,且捕获同名变量会报错。
  5. 5. 忽略对象成员的深拷贝需求:如果成员包含指针或资源,按值捕获只做浅拷贝,可能需要自定义拷贝行为。

面试中可能出现的问题

  1. 1. 解释捕获this和捕获*this的区别及使用场景。
  2. 2. 如何避免lambda捕获this导致的悬空指针问题?
  3. 3. 为什么捕获*this需要mutable
  4. 4. 捕获*this时,lambda闭包的大小如何变化?
  5. 5. 在多线程环境中,如何安全使用lambda捕获类成员?
  6. 6. C++14和C++17中捕获对象副本的写法区别?

总结

C++17的按值捕获*this是对lambda捕获机制的重大改进,既解决了传统捕获this指针的安全隐患,又提供了更简洁的语法。它体现了现代C++设计哲学中对安全、简洁和性能的平衡。掌握它不仅能写出更健壮的代码,还能在多线程、异步编程中避免难以发现的bug。
本文首发于【讳疾忌医-note】公众号,未经授权,不得转载。
(加入我的知识星球,免费获取账号,解锁所有文章。)

 

阅读剩余
THE END