【Effective Modern C++】第六章 lambda表达式:避免使用默认捕获模式

  • lambda 表达式 :代码中的匿名函数片段(比如std::find_if[](int val){ return 0<val && val<10; }这行),是编译期的代码片段;
  • 闭包 :lambda 在运行时创建的对象,持有捕获数据的副本 / 引用(比如传给find_if的第三个实参就是闭包);
  • 闭包类:编译器为每个 lambda 自动生成的唯一类,闭包是该类的实例,lambda 的逻辑会变成闭包类成员函数的执行代码。

避免使用 lambda 的默认捕获模式

C++11 有两种默认捕获模式:[&](按引用捕获)、[=](按值捕获)

1. 默认按引用捕获([&]):悬空引用风险
  • 原理 :按引用捕获会让闭包持有 "lambda 定义作用域内的局部变量 / 形参" 的引用;如果闭包的生命周期超过这些变量(比如闭包被存入容器,变量随函数返回销毁),引用就会变成悬空引用,后续使用闭包会触发未定义行为。

  • 典型例子

    void addDivisorFilter() {
    int divisor = computeDivisor(); // 局部变量
    filters.emplace_back(
    [&](int value) { return value % divisor == 0; } // 捕获divisor的引用
    );
    } // 函数返回,divisor销毁,filters中的闭包引用悬空

  • 补充 :即使显式按引用捕获([&divisor])也有悬空风险,但显式写变量名能提醒你关注生命周期,比默认[&]更易排查问题。

2. 默认按值捕获([=]):两大致命陷阱
陷阱 1:捕获this指针而非类成员变量,导致悬空指针
  • 原理 :捕获仅针对 "lambda 所在作用域的 non-static 局部变量 / 形参";类成员变量不是局部变量,lambda 中使用成员变量时,编译器会自动替换为this->成员名,因此默认按值捕获实际拷贝的是this指针,而非成员变量本身。
  • 典型例子
c++ 复制代码
class Widget {
private:
    int divisor;
public:
    void addFilter() const {
        filters.emplace_back(
            [=](int value) { return value % divisor == 0; } // 看似捕获divisor,实则拷贝this指针
        );
    }
};

void doSomeWork() {
    auto pw = std::make_unique<Widget>();
    pw->addFilter(); // 闭包持有Widget的this指针
} // Widget被销毁,filters中的闭包持有悬空的this指针
  • 解决方案

    • C++11:先把成员变量拷贝到局部变量,再捕获这个局部变量;
    • C++14(更优):用通用 lambda 捕获直接拷贝成员变量(无默认捕获模式,更安全):
c++ 复制代码
void Widget::addFilter() const {
    filters.emplace_back(
        [divisor = divisor](int value) { return value % divisor == 0; }
    );
}
陷阱 2:误导认为 lambda "独立",实际依赖 static 变量
  • 原理 :默认按值捕获仅拷贝 non-static 局部变量;lambda 中使用的static变量(全局、命名空间、静态局部)不会被捕获,而是直接引用。用户看到[=]易误以为 "lambda 拷贝了所有使用的变量,是独立的",但 static 变量的修改会影响所有 lambda 的行为。
  • 典型例子
c++ 复制代码
void addDivisorFilter() {
    static int divisor = computeDivisor(); // static变量
    filters.emplace_back(
        [=](int value) { return value % divisor == 0; } // 未捕获任何东西,直接引用static变量
    );
    ++divisor; // 每次调用递增,所有lambda都会使用新值
}

看似[=]让 lambda 独立,实则所有添加到 filters 的 lambda 都会随divisor递增而改变行为,违背预期。

替代默认捕获模式的最佳实践

  1. 原则 :完全避免默认捕获模式([&]/[=]),显式列出需要捕获的变量(比如[divisor]而非[=][&calc1]而非[&]);

  2. 捕获类成员

    • C++11:先拷贝成员到局部变量(auto divisorCopy = divisor;),再捕获这个局部变量;
    • C++14:用通用 lambda 捕获([divisor = divisor])直接拷贝成员;
  3. 警惕 static 变量:即使按值捕获,lambda 仍会引用 static 变量,需注意外部修改对 lambda 的影响。

总结

  1. 默认按引用捕获([&])易导致悬空引用(闭包生命周期长于捕获的局部变量);
  2. 默认按值捕获([=])有两大问题:一是捕获this指针而非类成员,易产生悬空指针;二是误导认为 lambda "独立",实际依赖 static 变量受外部修改影响;
  3. 最佳实践:避免所有默认捕获模式,显式捕获需要的变量;捕获类成员时,C++11 先拷贝到局部变量,C++14 用通用 lambda 捕获。

原著在线阅读地址

相关推荐
随意起个昵称1 小时前
建图优化小记
c++·算法
王老师青少年编程2 小时前
2020年信奥赛C++提高组csp-s初赛真题及答案解析(选择题6-10)
c++·题解·真题·初赛·信奥赛·csp-s·提高组
GIS阵地2 小时前
如何统计QGIS里栅格图层的面积呢
c++·qgis·开源gis·pyqgis
汉克老师10 小时前
GESP2024年6月认证C++二级( 第一部分选择题(9-15))
c++·循环结构·分支结构·gesp二级·gesp2级·求余数
王老师青少年编程11 小时前
csp信奥赛c++高频考点假期集训(分模块进阶)
数据结构·c++·算法·csp·高频考点·信奥赛·集训
王老师青少年编程12 小时前
2020年信奥赛C++提高组csp-s初赛真题及答案解析(选择题1-5)
c++·题解·真题·初赛·信奥赛·csp-s·提高组
plus4s13 小时前
2月18日(82-84题)
c++·算法·动态规划
wangluoqi14 小时前
c++ 树上问题 小总结
开发语言·c++
不梦闲人15 小时前
15 面向对象程序设计
c++