lambda表达式在使用位置创建一个匿名闭包对象。这个对象可以保存捕获的状态,并通过函数调用运算符执行代码;它与手写 operator()的函数对象属于同一类思路。本页合并原lambda教程、捕获列表速查和STL函数对象笔记,以C++17为主,另外标明C++20语法。
1. 语法中的每一部分
跳转到“1. 语法中的每一部分”[捕获列表](参数列表) mutable noexcept -> 返回类型 { 函数体 }mutable、noexcept和显式返回类型都按需要使用,不是每个lambda都必须填写。mutable改变闭包调用运算符的const属性;noexcept承诺异常不会逃出调用,不是用来声明“函数内可能throw”。让异常逃出noexcept函数会终止程序。
lambda本身是表达式,它产生一个独特类型的对象;auto function = ...保存该对象,function(arguments)调用它。闭包不等同于普通函数指针,只有符合条件的无捕获lambda能转换为相应函数指针。
2. 捕获列表速查:默认规则不是捕获全部变量
跳转到“2. 捕获列表速查:默认规则不是捕获全部变量”默认捕获只为函数体等使用规则要求捕获的外部局部实体提供方式,不会把整个作用域里每一个对象都无条件复制或引用。全局变量、静态存储对象等也不是按普通自动局部变量的方式捕获。
| 写法 | 意义/版本 |
|---|---|
[] | 不捕获自动局部变量,可使用参数和适用的全局/静态实体。 |
[x] / [&x] | 显式保存x的副本/引用x。 |
[=] / [&] | 需要隐式捕获时,默认按值/按引用。 |
[x, &y] | x为副本,y为引用。 |
[=, &x] | x显式引用,其他需要隐式捕获的局部实体按值。 |
[&, x] | x显式副本,其他需要隐式捕获的局部实体按引用。 |
[this] | 保存当前对象指针,用它访问成员;不复制整个对象。 |
[this, value] | 保存this指针和局部变量value的副本,value应是在此可捕获的局部名字。 |
[&, this] | 显式写this允许但冗余;其他需要捕获的局部实体按引用。 |
[=, this] | C++20起可显式写出this,同时保留默认值捕获。 |
[copy = expression] | C++14初始化捕获,可起新名字或移动资源。 |
[*this] | C++17捕获当前对象副本;其成员所指对象是否深复制仍由类型语义决定。 |
[this]既不延长当前对象寿命,也不是“把所有成员按引用捕获”的所有权机制。C++20起通过 [=]隐式捕获this被弃用;需要对象时明确写清意图。C++工作草案:捕获
3. 值、引用和mutable
跳转到“3. 值、引用和mutable”#include <cassert>
int main(){ int value = 10; auto snapshot = [value] { return value; }; auto localCounter = [value]() mutable { return ++value; }; auto changeOriginal = [&value] { value += 5; }; value = 20; assert(snapshot() == 10); assert(localCounter() == 11 && localCounter() == 12); assert(value == 20); changeOriginal(); assert(value == 25);
int scale = 2; int result = 0; auto mixed = [=, &result] { result = value * scale; }; scale = 100; mixed(); assert(result == 50);}按值捕获的副本默认不能经普通lambda调用修改,原稿 [=] { value += 5; }不能编译。加mutable之后修改的是闭包自己的副本,不会变成外部value。引用捕获修改所引用对象本身,无需仅为这件事添加mutable。
闭包副本也有自己的状态:复制上面的 localCounter会复制当前计数值,之后两个闭包可各自变化;这不自动为并发调用同一闭包提供同步。
4. this指针与对象副本
跳转到“4. this指针与对象副本”#include <cassert>
class Counter {public: explicit Counter(int value) : value_(value) {} auto observe() const { return [this] { return value_; }; } auto snapshot() const { return [*this] { return value_; }; } void set(int value) { value_ = value; }private: int value_;};
int main(){ Counter counter(42); auto observe = counter.observe(); auto snapshot = counter.snapshot(); counter.set(99); assert(observe() == 99 && snapshot() == 42);}observe必须在counter仍存活时调用;返回闭包不会延长counter的寿命。snapshot拥有本例简单对象的副本,但如果原类里只有某资源的裸指针,复制该指针不会自动复制资源。QObject等不可复制类型也不能任意使用 [*this]。
GUI异步回调除了捕获方式,还要管理上下文对象和所访问资源的寿命;Qt场景见连接类型与生命周期。不要让离开作用域后的lambda继续引用局部变量。
5. 初始化捕获与通用参数
跳转到“5. 初始化捕获与通用参数”#include <cassert>#include <memory>#include <utility>#include <vector>
int main(){ auto number = std::make_unique<int>(42); auto own = [number = std::move(number)] { return *number; }; assert(!number && own() == 42);
auto sizeOf = [](const auto& values) { return values.size(); }; auto stored = [values = std::vector<int>{1, 2, 3, 4}] { return values.size(); }; assert(sizeOf(std::vector<int>{1, 2}) == 2 && stored() == 4);}初始化捕获和auto参数都从C++14可用。[]<typename T>(T value)形式的显式模板形参列表则属于C++20;不能只因编译器较新便把它标成C++17例子。移动捕获unique_ptr后,闭包通常只能移动,不能要求它像普通可复制对象一样使用。
6. 手写函数对象与算法
跳转到“6. 手写函数对象与算法”“函数对象”是可调用的对象,不是某一种特殊类的名字。手写一个带 operator()的类,其实例就能作为算法参数;lambda提供更局部的写法。
#include <algorithm>#include <cassert>#include <sstream>#include <vector>
struct Print { std::ostream& output; void operator()(int value) const { output << value << ' '; }};
int main(){ std::vector<int> values{1, 2, 3, 4, 5}; std::ostringstream output; std::for_each(values.begin(), values.end(), Print{output}); assert(output.str() == "1 2 3 4 5 ");
int evenCount = 0; std::for_each(values.begin(), values.end(), [&evenCount](int value) { if (value % 2 == 0) ++evenCount; }); assert(evenCount == 2); std::for_each(values.begin(), values.end(), [](int& value) { value *= 2; }); assert((values == std::vector<int>{2, 4, 6, 8, 10}));}原来的 [](int x) { cout << x * 2; }只输出两倍值,既不改变容器元素,也不会因为搭配for_each就保存结果。要原地修改元素,用引用形参;要生成另一序列,考虑transform及有效输出范围。
标准函数对象
跳转到“标准函数对象”<functional>中已有通用运算包装:
| 类型 | 对应运算 |
|---|---|
plus/minus/multiplies/divides/modulus/negate | + - * / %与一元负号。 |
equal_to/not_equal_to/greater/less/greater_equal/less_equal | 比较,结果用于等价判断、排序等。 |
logical_and/logical_or/logical_not | 逻辑与、或、非;函数调用的实参求值不等于内建&&/||的短路求值。 |
比较函数对象不应放在“逻辑函数对象”之下混为一类。std::plus<int>{}(2,3)返回5;透明写法如 std::less<>{}从C++14可用,常用于类型可由调用参数推导的场景。运算包装不会自动处理溢出或除零等前提。C++工作草案:函数对象
7. 高阶调用与std::function
跳转到“7. 高阶调用与std::function”高阶函数接收或返回可调用对象。可用模板保留具体类型;需要运行时统一保存具有同一调用签名的目标时,可以用 std::function进行类型擦除。
#include <cassert>#include <functional>
template<class Function>int apply(Function function, int a, int b){ return function(a, b);}
int main(){ const auto add = [](int a, int b) { return a + b; }; std::function<int(int, int)> stored = add; assert(apply(add, 10, 20) == 30 && stored(10, 20) == 30);
int (*functionPointer)(int) = [](int value) { return value + 1; }; assert(functionPointer(4) == 5); std::function<void()> empty; assert(!empty); bool rejected = false; try { empty(); } catch (const std::bad_function_call&) { rejected = true; } assert(rejected);}在C++17中,std::function要求其保存的目标可复制,所以不能直接保存上面独占unique_ptr的移动闭包。它也不自动延长引用捕获目标的寿命,可能带来类型擦除和分配开销;不需要动态统一类型时,直接用auto或模板通常更简单。
8. 抛异常与noexcept分开说明
跳转到“8. 抛异常与noexcept分开说明”#include <cassert>#include <limits>#include <stdexcept>
int main(){ const auto divide = [](int a, int b) -> int { if (b == 0) throw std::invalid_argument("division by zero"); if (a == std::numeric_limits<int>::min() && b == -1) throw std::overflow_error("integer quotient is not representable"); return a / b; }; assert(divide(7, 2) == 3); bool zero = false, overflow = false; try { (void)divide(1, 0); } catch (const std::invalid_argument&) { zero = true; } try { (void)divide(std::numeric_limits<int>::min(), -1); } catch (const std::overflow_error&) { overflow = true; } assert(zero && overflow);
const auto nonnegative = [](int value) noexcept { return value >= 0; }; static_assert(noexcept(nonnegative(0))); assert(nonnegative(0));}这保留原整数除法场景,同时处理零除数与最小int除以−1的不可表示结果。返回类型写成 -> int并不是异常规范。是否标noexcept取决于真实行为,不能为追求“安全”把会抛出的调用一律标noexcept。