ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

C++中的Lambda表达式及表达式语句

2026/10/8 8:40:11 拓冰建站 浏览量
C++中的Lambda表达式及表达式语句 前言lambda 表达式lambda expression在 C11 进入标准C14 补上了泛型参数和初始化捕获C17 又加了按值复制整个对象的[*this]捕获。到 C17 为止它在日常代码里的使用频率已经超过了很多传统语法。但 lambda 也是最容易写出悬垂引用的地方之一。原因在于它的捕获列表看起来像一个语法糖实际上决定了闭包对象里到底存了什么存的是副本、是指针、还是一个引用。三条最容易踩的线用[]捕获局部变量然后把 lambda 存起来、返回出去或者交给异步任务——局部变量销毁后闭包里的引用就悬垂了再调用它是未定义行为UB。用[]在成员函数里捕获以为按值就不会出事其实[]抓到的是this指针对象销毁后照样悬垂。以为捕获了指针就安全——指针是按值拷贝的但它指向的对象并不会因此延长生命。另一半标题里的表达式语句也值得说清lambda 是一个表达式不是语句。它的类型是编译器为每个 lambda 单独生成的、独一无二的闭包类型closure type。理解这一点就能理解为什么[]{ foo(); };这种写法编译得过却什么都不做。本文以 C17 为基准代码在 GCC 13 / Clang 17 / MSVC 19.3x 上均可编译。一、lambda 的语法与求值模型完整形式是[捕获列表](参数列表) 说明符 异常规范 - 返回类型 { 函数体 }除捕获列表和函数体外其余都可以省略。一个 lambda 表达式求值的结果是一个闭包对象closure object它的类型是编译器生成的、程序里其他任何地方都写不出来的唯一类型所以只能用auto或模板参数来接。闭包的调用运算符operator()默认是const的这意味着按值捕获的副本在函数体里默认不可修改想改就得加mutable。没有捕获的 lambda 可以隐式转换成函数指针C11 起条件是它没有捕获、不是泛型参数里没有auto。这一条让 lambda 能直接传给 C 风格的 API。#include algorithm #include cstddef #include iostream #include string #include vector int main() { // 无捕获 lambda 可以隐式转成函数指针 int (*fp)(int) [](int x) { return x * 2; }; std::cout fp(21) \n; // 42 std::vectorint v{5, 1, 4, 2, 3}; // 按值捕获 limit、按引用捕获 v显式写出返回类型 const int limit 3; const auto count_gt [limit, v]() - std::size_t { return static_caststd::size_t( std::count_if(v.begin(), v.end(), [limit](int x) { return x limit; })); }; std::cout count_gt() \n; // 5 和 4 大于 3 - 2 // mutable修改的是闭包里的副本不影响外部的 counter int counter 0; auto bump [counter]() mutable { return counter; }; std::cout bump() bump() counter \n; // 1 2 0 // 初始化捕获C14把表达式的结果直接放进闭包 auto owned [s std::string(hello)]() { return s.size(); }; std::cout owned() \n; // 5 // 泛型 lambdaC14参数写 auto本质是个模板化的 operator() const auto twice [](const auto x) { return x x; }; std::cout twice(21) twice(std::string(ab)) \n; std::sort(v.begin(), v.end(), [](int a, int b) { return a b; }); std::cout v.front() \n; // 5 return 0; }注意twice是泛型 lambda能用auto参数这件事在 C14 才有C11 下必须写成接受具体类型或者改用函数模板。二、捕获列表的七种形态写法含义引入版本[]不捕获可转成函数指针C11[]默认按值捕获被用到的自动变量C11[]默认按引用捕获被用到的自动变量C11[x]/[x]单独指定某个变量按值 / 按引用捕获C11[this]捕获this指针C11[*this]按值复制整个对象副本进闭包C17[x 表达式]初始化捕获可配合std::moveC14几条必须记牢的语义按值捕获是捕获那一刻的拷贝之后外部怎么改都不影响闭包里的副本闭包改副本也影响不到外部。按引用捕获不延长任何东西的生命它只是一个引用被引用对象没了它就悬垂。[]和[]是默认捕获只对函数体里实际用到的自动变量生效没用到的不会被捕获。[]抓到this时抓的是指针的副本不是对象的副本。另外C20 起[]隐式捕获this被标记为弃用需要显式写成[, this]。这是 C20 的改动以 C17 为基准写代码时不适用但做版本迁移时要注意。三、this 捕获与返回闭包悬垂的两大来源把 lambda 存进std::function返回出去是最常见的悬垂写法。下面这段代码把三类错误和它们的正确版本放在一起#include cstddef #include functional #include iostream #include string #include utility // ❌ 引用捕获局部变量闭包被返回后引用的对象已销毁再调用是 UB std::functionint() make_bad() { int local 42; return [local]() { return local; }; } // ✅ 按值捕获闭包自己持有副本 std::functionint() make_ok() { int local 42; return [local]() { return local; }; } // ✅ 初始化捕获 移动把资源移进闭包避免多余拷贝 std::functionstd::size_t() make_owned() { std::string big(100, x); return [s std::move(big)]() { return s.size(); }; } int main() { const auto f make_ok(); std::cout f() \n; // 42 std::cout make_owned()() \n; // 100 return 0; }std::function要求被包装的可调用对象可拷贝。上面三个闭包都只持有可拷贝的成员所以没问题。如果捕获了std::unique_ptr这类仅可移动的类型闭包就不可拷贝装不进std::functionC23 的std::move_only_function是为这种情况准备的在 C17 下只能用模板参数或者std::shared_ptr绕过。第二类悬垂来自this。这是成员函数里最隐蔽的 bug#include functional #include iostream class Widget { public: explicit Widget(int v) : v_(v) {} // ❌ [] 在这里抓的是 this 指针不是 v_ 的副本 std::functionint() bad() { return []() { return v_; }; // 等价于捕获了 this } // ✅ 先把成员复制到一个局部变量再按值捕获它 std::functionint() good() { const int local_copy v_; return [local_copy]() { return local_copy; }; } // ✅ C17 的 [*this]按值复制整个对象闭包不再依赖 this std::functionint() also_good() { return [*this]() { return v_; }; } private: int v_; }; int main() { std::functionint() f; { Widget w(7); f w.good(); // 换成 w.bad() 就是悬垂调用是 UB } // w 在这里销毁 std::cout f() \n; // 7 return 0; }[*this]是 C17 引入的它让闭包持有整个对象的副本。代价是复制成本而且它要求该类可拷贝。在 C17 之前只能用上面good()那种手工复制每个用到的成员的办法。四、lambda 是表达式表达式语句里的常见写法lambda 是表达式所以它可以出现在任何表达式能出现的位置初始化器、函数实参、return语句、甚至条件表达式。#include iostream int main() { // 立即调用的 lambdaIIFE定义 调用写在一起 const int x []() - int { return 6 * 7; }(); std::cout x \n; // 42 // 整条是一个表达式语句构造闭包、调用它、丢弃返回值 [x]() { std::cout x x \n; }(); // 无捕获闭包是空类空类对象的大小至少 1 字节 const auto noop [] {}; std::cout sizeof(noop) \n; return 0; }关于表达式语句expression statement本身要清楚三点表达式语句的形式是表达式加分号它的值被丢弃这叫被丢弃值表达式discarded-value expression。一条只有 lambda 的语句比如[]{ do_something(); };只是构造了一个闭包对象然后扔掉函数体根本不会执行。GCC 会给出 statement has no effect 之类的警告其他编译器不一定。IIFEImmediately Invoked Function Expression立即调用的函数表达式就是构造完立刻调用写成[]{ ... }();——注意最后那对调用括号它就是 lambda 和 IIFE 的区别。把一个只有 lambda 的表达式当语句写是初学者最常见的代码看起来对、什么都没发生的情况。常见坑点场景❌ 错误写法✅ 正确写法说明返回闭包return [local]() { return local; };return [local]() { return local; };引用捕获不延长生命返回后调用是 UB成员函数返回闭包return []() { return v_; };复制成员后按值捕获或 C17 用[*this][]抓的是this指针对象销毁后悬垂空语句[]{ foo(); };[]{ foo(); }();前者只是构造了一个闭包就丢弃foo 永远不会被调用修改副本[n]() { n; }编译不过[n]() mutable { n; }闭包的operator()默认是const的误以为改了外部[n]() mutable { n; }后检查外部的n需要改外部就用[n]mutable改的是闭包内的副本泛型 lambda 的版本C11 下写[](auto x) { ... }C14 起才支持或改用函数模板泛型 lambda 是 C14 特性结构化绑定捕获C17 下直接捕获结构化绑定的名字C20 起才支持C17 下先拷贝到普通变量结构化绑定捕获是 C20 特性仅可移动的捕获把捕获了std::unique_ptr的 lambda 塞进std::function改用模板参数或用std::shared_ptrstd::function要求可拷贝std::move_only_function要 C23总结主题关键结论类型每个 lambda 表达式有唯一的闭包类型只能用auto/ 模板参数接无捕获 lambda可隐式转成函数指针能直接传给 C 风格 API按值捕获存的是捕获时刻的副本外面怎么改都不影响按引用捕获只是一个引用绝不延长生命周期是悬垂的头号来源[]与this[]在成员函数里抓到的是this指针C17 下如此C20 起需要在捕获列表里显式写this[*this]C17 起可用按值复制整个对象进闭包表达式语句lambda 是表达式单独一条只有 lambda 的语句什么都不会执行用 lambda 只需要问自己两个问题闭包里存的是副本还是引用这个闭包会不会活得比它引用的东西更久把这两点想清楚[]还是[]、要不要mutable、用不用[*this]答案自然就出来了。