找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4890

积分

0

好友

639

主题
发表于 昨天 19:12 | 查看: 4| 回复: 0

Lambda 表达式凭借简洁优雅的语法,成了现代 C++ 最受欢迎的特性之一。

但好用不代表安全。有时候,一个运行了好几天的进程突然 coredump,即使 gdb 调试也看不出问题在哪。问题很可能就藏在你不经意的捕获方式里。

快速回顾

先快速回顾一下 Lambda 的几种捕获方式,方便大家进入下面的内容:

int x = 42;
std::string name = "hello";

auto by_value = [x, name]() {};
auto by_ref   = [&x, &name]() {};
auto all_val  = [=]() {};
auto all_ref  = [&]() {};

简单总结如下:

捕获 含义
[x] 拷贝 x
[&x] 引用 x
[=] 拷贝所有用到的变量
[&] 引用所有用到的变量

带着这个直觉,我们开始下面的踩坑之旅。

坑一:成员函数内使用 [=]

示例如下:

class Server {
    std::string host_ = "localhost";
    int port_ = 8080;

public:
    auto handler() {
        return [=]() {
            std::cout << host_ << ":" << port_;
        };
    }
};

很多人以为捕获的是 host_port_ 的副本。实际上,此处捕获的是 this 指针。

看下面这个用法:

Server server;
auto handler = server.get_handler();

// server 对象销毁

handler();  // 指针悬空,未定义行为

我们可以用下面这种方法避免:

auto get_handler() {
    return [host = host_, port = port_]() {
        std::cout << host << ":" << port << "\n";
    };
}

或者用这个(C++17 起支持 [*this]):

auto get_handler() {
    return [*this]() {
        std::cout << host_ << ":" << port_ << "\n";
    };
}

坑二:引用捕获的悬空陷阱

常见用法如下:

std::function<int()> make_counter() {
    int count = 0;
    return [&count]() {
        return ++count;
    };
}

auto counter = make_counter();
counter();

当我们调用 make_counter() 后,其栈帧销毁:Lambda 持有 &count → 已释放的栈内存。结果就是未定义行为。

项目中更隐蔽的场景:

void setup() {
    auto config = load_config();

    register_callback([&]() {
        use(config); // 稍后执行 → config 已死
    });
}

也就是说,只要涉及回调、线程、协程、容器、返回值等场景,就应该禁用引用捕获,改用按值捕获:

register_callback([config]() { use(config); });

坑三:Move 捕获与 mutable

auto ptr = std::make_unique<Widget>();

auto lambda = [p = std::move(ptr)]() {
    p->do_thing();
};

上面代码一切正常,但如果再加上其他操作:

auto lambda = [p = std::make_unique<Widget>()]() {
    p->do_thing();          // OK,只能调用 const 成员函数
    p = nullptr;            // 编译失败
    p.reset();              // 编译失败
    transfer(std::move(p)); // 编译失败
};

上述代码编译失败,这是因为 Lambda 的 operator() 默认是:

void operator()() const;

即捕获变量 = const 成员。可以这样解决:

auto f = [p = std::make_unique<Foo>()]() mutable {
    p.reset();
};

需要注意的是,使用 mutable 会改变其内部状态:

auto counter = [n = 0]() mutable { return ++n; };
std::cout << counter();  // 1
std::cout << counter();  // 2
std::cout << counter();  // 3

坑四:复制 mutable Lambda 的状态独立

auto counter = [n = 0]() mutable { return ++n; };

auto copy = counter;

counter(); // 1
counter(); // 2
copy();    // 1 不是 3

这是因为每个 Lambda 都有自己的状态副本。

项目中常见的一种用法:

std::function<int()> fn = counter;  // 内部复制

我们可能以为 fncounter 共享状态,其实并不是,它们是完全独立的对象。

如果需要共享状态,可以这样:

auto n = std::make_shared<int>(0);
auto counter = [n]() { return ++*n; };

这样通过智能指针实现了真正的状态共享。在云栈社区的技术交流中,经常有开发者在这里踩坑,尤其是涉及多线程回调时更需注意。

坑五:结构化绑定的捕获(C++17 vs C++20)

auto [x, y] = std::make_pair(1, 2);

// C++17: can't capture structured bindings
// C++20: allowed
auto lambda = [x, y]() { return x + y; };

C++20 终于允许在 Lambda 中捕获结构化绑定,但它的行为取决于绑定本身是不是引用,这点非常容易误解。

也就是说:

std::pair<int, int> some_pair{1, 2};

auto [a, b] = some_pair;

auto lambda = [a, b]() {
    return a + b;
};

ab 是普通变量,Lambda 捕获的是它们的副本。上述代码等价于:

auto lambda = [a_copy = a, b_copy = b]() {
    return a_copy + b_copy;
};

而对于引用绑定:

std::pair<int, int> some_pair{1, 2};

auto& [a, b] = some_pair;   // 注意这里的 &

auto lambda = [a, b]() {
    return a + b;
};

ab 本身就是引用,Lambda 捕获的是引用的副本。那么如果 some_pair 先销毁:

auto make_lambda() {
    std::pair<int, int> p{1, 2};
    auto& [a, b] = p;

    return [a, b]() { return a + b; };  // ← UB
}

这就会导致悬空引用。

坑六:捕获参数包(C++20 的 Pack Capture)

C++20 之前的版本中,不支持直接捕获参数包,必须用其他方式封装:

template <typename... Args>
auto wrap(Args&&... args) {
    auto lambda =
        [tup = std::make_tuple(std::forward<Args>(args)...)]() mutable
    {
        std::apply(
            [](auto&... xs) {
                (process(std::move(xs)), ...);
            },
            tup
        );
    };

    return lambda;
}

自 C++20 起,引入 pack capture,上面代码可以简化为:

template <typename... Args>
auto wrap(Args... args) {
    auto lambda = [...args = std::move(args)]() {
        (process(args), ...);
    };

    return lambda;
}

坑七:默认捕获与显式捕获混合

来看一个合法示例:

int a = 1, b = 2, c = 3;

// 除 a 外全部引用捕获
auto lambda = [&, a]() {
    // a → 值捕获
    // b → 引用捕获
    // c → 引用捕获
};

// 除 b 外全部值捕获
auto lambda2 = [=, &b]() {
    // a → 值捕获
    // b → 引用捕获
    // c → 值捕获
};

如果需要捕获的变量很多,比如几十个:

auto lambda = [&, config, timeout]() {
    send_request(
        socket,
        host,
        port,
        buffer,
        parser,
        handler,
        config,
        timeout);
};

这时,你不得不仔细想清楚:哪个是引用捕获?哪个是值捕获?

针对这种情况,建议使用显式捕获:

auto lambda =
    [&socket,
     &buffer,
     config,
     timeout]()
{
    socket.send(buffer, config, timeout);
};

结语

Lambda 捕获是那种「语法简单、坑极深」的特性。[=] 看着安全,[&] 看着方便,却常常在生产环境、高并发下出现意想不到的问题。

解决方案其实很朴素:显式捕获

当然,最最最重要的是记住:成员函数中的 [=] 捕获的是 this,而不是成员变量。

以上。

如果对本文有疑问,欢迎到云栈社区 C/C++ 板块一起讨论交流,那里有更多关于智能指针、移动语义、模板元编程等深度话题。




上一篇:C++编译器为何生成多个析构函数?Itanium ABI与虚析构全解析
下一篇:Linux TCP连接数65535并非服务器并发上限,那如何应对百万并发?
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-4-25 00:24 , Processed in 0.828311 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表