跳转到内容
新建笔记

C++ Lambda 与函数对象:捕获、调用和生命周期

lambda表达式在使用位置创建一个匿名闭包对象。这个对象可以保存捕获的状态,并通过函数调用运算符执行代码;它与手写 operator()的函数对象属于同一类思路。本页合并原lambda教程、捕获列表速查和STL函数对象笔记,以C++17为主,另外标明C++20语法。

[捕获列表](参数列表) 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++工作草案:捕获

#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会复制当前计数值,之后两个闭包可各自变化;这不自动为并发调用同一闭包提供同步。

#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。