【C++】Lambda 表达式
C++ Lambda 表达式
一、基本语法结构
[捕获列表]<模板参数>(函数参数) 说明符 noexcept -> 返回类型 requires 约束 { 函数体 }
其中 [] 和函数体必须存在;模板参数(C++20)、函数参数、说明符、noexcept、尾置返回类型和约束等都是按需使用的可选部分。常见说明符包括 mutable、constexpr(C++17)、consteval(C++20)和 static(C++23)。static lambda 必须无捕获,且不能与 mutable 同时使用;constexpr 和 consteval 也不能同时出现。最简形式为:
[]{}; // 合法:无捕获、无参数、无返回值、空函数体
[](){}; // 等价写法
二、捕获列表(Capture List)——核心重点
捕获列表决定 lambda 如何访问定义它的外部作用域中的变量。
2.1 常见捕获形式一览
| 写法 | 含义 |
|---|---|
[] | 不捕获任何外部变量 |
[x] | 按值捕获 x(拷贝一份) |
[&x] | 按引用捕获 x |
[=] | 默认按值捕获 lambda 中需要使用的可捕获局部实体 |
[&] | 默认按引用捕获 lambda 中需要使用的可捕获局部实体 |
[=, &x] | 默认按值,但 x 按引用 |
[&, x] | 默认按引用,但 x 按值 |
[x = expr] | 初始化捕获(C++14),用表达式初始化 |
[this] | 捕获当前对象的 this 指针 |
[*this] | 捕获当前对象的副本(C++17) |
[args...] | 按值捕获参数包(模板中) |
[&args...] | 按引用捕获参数包(模板中) |
[..., xs = expr] | 初始化捕获参数包(C++20) |
更精确地说,默认捕获主要针对自动存储期的局部实体。全局变量和静态局部变量可以直接访问,不需要放入捕获列表;成员函数中的
this另有特殊捕获规则,见 2.6 和 11.4 节。
2.2 按值捕获 [x] / [=]
int x = 10;
auto f = [x]() {
std::cout << x << std::endl; // 输出 10
};
x = 99;
f(); // 仍然输出 10!捕获的是创建闭包对象时的副本
⚠️ 对普通按值捕获的非
const对象,lambda 默认不能修改闭包中的副本;加上mutable后才可以修改该副本。mutable不会移除对象类型本身的const。
2.3 按引用捕获 [&x] / [&]
int x = 10;
auto f = [&x]() {
x += 5; // ✅ 可以修改
};
f();
std::cout << x; // 输出 15
⚠️ 引用不延长生命周期。若外部变量先销毁,lambda 内访问就是悬空引用(UB)。
2.4 混合捕获
int a = 1, b = 2, c = 3;
auto f = [=, &b]() { // 实际使用的 a、c 按值;b 按引用
std::cout << a << c; // 读取 a、c,因此它们会被捕获
// a++; ❌ 只读副本
b++; // ✅ 修改外部 b
// c++; ❌ 只读副本
};
2.5 初始化捕获与移动捕获(C++14)
auto ptr = std::make_unique<int>(42);
// 将 ptr 移动进 lambda,外部 ptr 变为 nullptr
auto f = [p = std::move(ptr)]() {
std::cout << *p << std::endl; // 42
};
// ptr 此时已为空
也可以用来重命名:
int x = 100;
auto f = [y = x + 1]() { // y = 101,与外部 x 无关
std::cout << y;
};
2.6 捕获 this 与 *this
struct Foo {
int val = 42;
auto get_by_this() {
return [this]() { return val; }; // 捕获 this 指针
}
auto get_by_copy() {
return [*this]() { return val; }; // C++17,捕获对象副本
}
};
[this]:lambda 内通过指针访问对象,对象销毁则悬空。[*this]:lambda 持有当前对象的一份拷贝,可避免仅因原对象销毁而造成的this悬空;但对象必须可拷贝,且其内部借用的资源仍需要单独保证生命周期。
2.7 捕获列表的作用域
捕获列表中的名字在 lambda 体内遮蔽外部同名变量:
int x = 1;
auto f = [x = 99]() {
std::cout << x; // 99,不是外部的 1
};
三、参数列表
3.1 普通参数
auto add = [](int a, int b) { return a + b; };
add(3, 4); // 7
3.2 泛型参数(C++14)——auto 参数
using namespace std::string_literals;
auto add = [](auto a, auto b) { return a + b; };
add(3, 4); // int
add(3.0, 4.0); // double
add("a"s, "b"s); // string
底层等价于成员函数模板:
template<typename T, typename U>
auto operator()(T a, U b) const { return a + b; }
3.3 模板参数列表(C++20)
auto f = []<typename T>(std::vector<T>& v) {
return v.size();
};
比 C++14 的 auto 更精确,可以约束类型、提取 T。
3.4 参数包(C++20)
auto sum = []<typename... Ts>(Ts... args) {
return (args + ...); // 折叠表达式
};
sum(1, 2, 3, 4); // 10
3.5 默认参数
auto f = [](int x, int y = 10) { return x + y; };
f(5); // 15
f(5, 3); // 8
3.6 无参数时括号可省略
auto f1 = []() { return 1; };
auto f2 = [] { return 1; }; // 等价
四、mutable 关键字
对普通的非静态、不含显式对象参数的 lambda,函数调用运算符 operator() 默认是 const 成员函数,因此普通按值捕获的非 const 副本默认不能修改。加 mutable 后,operator() 不再是 const,便可修改这些副本(不影响外部):
int x = 10;
auto f = [x]() mutable {
x++; // ✅ 修改的是副本
std::cout << x; // 11
};
f();
f(); // 12(副本状态保留在闭包对象中)
std::cout << x; // 10,外部不变
mutable 只改变闭包调用运算符的 const 属性,不会移除捕获对象类型本身的 const,也不决定指针所指对象或引用捕获对象是否可修改:
const int cx = 10;
auto g = [cx]() mutable {
// cx++; // ❌ cx 的类型本身就是 const int
};
int y = 0;
auto h = [p = &y]() {
++*p; // ✅ 未重新赋值 p,只修改 p 指向的对象,无需 mutable
};
C++23 显式对象参数是上述默认规则的例外:调用对象的 cv/ref 属性由显式对象参数决定,且这种 lambda 不能使用 mutable。static lambda 的 operator() 则是静态成员函数,没有隐式对象。
五、返回类型
5.1 自动推断
auto f = [](int x) { return x * 2; }; // 推断为 int
auto g = [](int x) { return x * 2.0; }; // 推断为 double
auto h = [](int x) { std::cout << x; }; // 推断为 void
5.2 多分支时必须一致(或用尾置返回类型)
// ❌ 编译错误:两个分支返回不同类型
auto bad = [](bool flag) {
if (flag) return 1;
else return 1.5;
};
// ✅ 显式指定
auto good = [](bool flag) -> double {
if (flag) return 1;
else return 1.5;
};
5.3 尾置返回类型语法
auto f = [](int a, int b) -> int {
return a + b;
};
5.4 返回引用(少见但合法)
int& get_ref(int& r) { return r; } // 普通函数对比
auto f = [](int& r) -> int& { return r; };
六、Lambda 的类型与 auto
6.1 每个 lambda 表达式都产生唯一类型
auto f1 = [](int x) { return x; };
auto f2 = [](int x) { return x; };
// f1 和 f2 类型不同!即使签名一模一样
// f1 = f2; ❌ 编译错误
6.2 用 std::function 统一类型
#include <functional>
std::function<int(int)> f1 = [](int x) { return x * 2; };
std::function<int(int)> f2 = [](int x) { return x + 1; };
f1 = f2; // ✅ 可以互相赋值
// 代价:类型擦除和间接调用;超出实现的小对象存储时还可能分配堆内存
6.3 Lambda 转函数指针(无捕获且无显式对象参数)
auto lam = [](int x) { return x * 2; };
int (*fp)(int) = lam; // ✅ 无捕获,可隐式转换
int y = 5;
auto lam2 = [y](int x) { return x * y; };
// int (*fp2)(int) = lam2; ❌ 有捕获,不能转换
从 C++23 起还需注意:即使 lambda 无捕获,只要含有显式对象参数,也不会生成到普通函数指针的转换。
七、Lambda 的常见使用场景
7.1 STL 算法(最经典)
std::vector<int> v = {3, 1, 4, 1, 5, 9};
// 排序
std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; });
// 查找
auto it = std::find_if(v.begin(), v.end(), [](int x) { return x > 4; });
// 计数
int cnt = std::count_if(v.begin(), v.end(), [](int x) { return x % 2 == 0; });
// 变换
std::transform(v.begin(), v.end(), v.begin(), [](int x) { return x * 2; });
// 累加
int sum = std::accumulate(v.begin(), v.end(), 0, [](int acc, int x) { return acc + x; });
7.2 回调函数
void register_callback(std::function<void(int)> cb) {
// ... 某事件触发时调用 cb(42);
}
int main() {
int threshold = 10;
register_callback([threshold](int val) {
if (val > threshold)
std::cout << "Exceeded!\n";
});
}
7.3 自定义比较器
// map 自定义排序
auto cmp = [](const std::string& a, const std::string& b) {
if (a.size() != b.size())
return a.size() < b.size();
return a < b; // 长度相同时再按字典序区分不同字符串
};
std::map<std::string, int, decltype(cmp)> my_map(cmp);
// 优先队列
auto cmp2 = [](int a, int b) { return a > b; }; // 小顶堆
std::priority_queue<int, std::vector<int>, decltype(cmp2)> pq(cmp2);
7.4 立即调用(IIFE)
const int x = [&]() {
int tmp = compute();
tmp += adjust();
return tmp * 2;
}(); // 注意末尾的 ()
常用于初始化 const 变量需要多步计算的场景。
7.5 递归 Lambda
// 方式1:std::function(有类型擦除开销)
std::function<int(int)> fib1 = [&](int n) -> int {
return n <= 1 ? n : fib1(n - 1) + fib1(n - 2);
};
// 方式2:通过引用参数传入自身(C++14)
auto fib2 = [](const auto& self, int n) -> int {
return n <= 1 ? n : self(self, n - 1) + self(self, n - 2);
};
fib2(fib2, 10);
// 方式3:C++23 显式对象参数(deducing this)
// auto fib = [](this auto self, int n) -> int { ... };
7.6 在类中作为成员函数的一部分
struct Widget {
int id;
void process(std::vector<int>& data) {
// 捕获 this 访问成员
std::for_each(data.begin(), data.end(), [this](int& x) {
x += id; // 通过 this->id
});
}
};
八、constexpr Lambda(C++17)
// C++17 起,满足条件时 operator() 可以隐式成为 constexpr
constexpr auto square = [](int x) { return x * x; };
// 也可以在参数列表后显式标注 constexpr
constexpr auto cube = [](int x) constexpr { return x * x * x; };
constexpr int val = square(5); // 编译期求值 = 25
static_assert(square(3) == 9);
static_assert(cube(3) == 27);
具体调用能否用于常量表达式,取决于该次求值是否满足常量求值规则。例如,常量求值期间不能执行 I/O;C++20 起允许受限制的动态内存分配,不应简化为“禁止动态内存”。
九、Lambda 与模板
9.1 作为模板参数
template<typename Func>
void apply(Func f, int x) {
f(x);
}
apply([](int x) { std::cout << x; }, 42);
9.2 作为模板参数(C++20 概念约束)
template<typename F>
requires std::invocable<F&, int&>
void apply(F f, int x) {
f(x);
}
函数体中的 f 和 x 都是左值,因此约束应检查 F& 能否接受 int&,使约束与真正的调用表达式 f(x) 保持一致。
9.3 捕获模板参数包
template<typename... Args>
auto make_logger(Args... args) {
// C++20:通过初始化捕获将参数包逐个移入 lambda
return [...args = std::move(args)]() {
((std::cout << args << " "), ...);
};
}
十、Lambda 的存储与传递方式对比
| 方式 | 写法 | 开销 | 适用场景 |
|---|---|---|---|
auto | auto f = []{...}; | 保留具体闭包类型,便于优化 | 局部使用 |
| 模板参数 | template<typename F> | 无类型擦除,通常便于内联 | 函数接收回调 |
std::function | std::function<void()> f | 类型擦除与间接调用;可能堆分配 | 需统一存储不同类型的可调用对象 |
| 函数指针 | void(*fp)() | 间接调用 | 仅限无捕获且无显式对象参数的 lambda |
// 用模板接收:无类型擦除,通常便于编译器内联
template<typename F>
void run(F&& func) {
std::forward<F>(func)();
}
十一、生命周期与常见陷阱
11.1 悬空引用
std::function<void()> make() {
int x = 42;
return [&x]() { std::cout << x; }; // ❌ x 在函数返回后销毁
}
修复:改为值捕获 [x] 或初始化捕获 [x = x]。
11.2 捕获循环变量(经典坑)
std::vector<std::function<void()>> funcs;
for (int i = 0; i < 5; i++) {
funcs.push_back([&i]() { std::cout << i; }); // ❌ 全部捕获同一个 i 的引用
}
// 离开 for 语句后 i 已销毁,此时调用任一回调都是未定义行为
// 修复:在循环体中值捕获当前 i
std::vector<std::function<void()>> safe_funcs;
for (int i = 0; i < 5; i++) {
safe_funcs.push_back([i]() { std::cout << i; });
}
for (const auto& func : safe_funcs) {
func(); // 依次输出 01234
}
11.3 捕获 this 后对象销毁
struct Widget {
void doSomething() {}
auto make_callback() {
return [this]() { doSomething(); };
}
};
auto callback = Widget{}.make_callback();
// 上一行结束时临时 Widget 已销毁,callback 内的 this 已悬空
callback(); // ❌ 未定义行为
若回调的生命期可能超过原对象,可在对象可拷贝时考虑 [*this],或通过明确的共享所有权管理对象生命周期。
11.4 [=] 在成员函数中的隐式 this 捕获(C++20 已弃用)
struct Foo {
int val = 10;
auto f() {
return [=]() { return val; }; // C++20 起仍会隐式捕获 this,但该写法已弃用
// 建议显式写 [this]、[=, this] 或 [*this]
}
};
十二、Lambda 与 noexcept
auto f = [](int x) noexcept { return x * 2; };
static_assert(noexcept(f(1))); // true
noexcept 修饰的是 lambda 的函数调用运算符 operator(),用于表达“调用该 lambda 不抛出异常”的契约。泛型代码可以通过 noexcept(f(args...)) 检查这一性质,并在自身的异常规格或实现策略中使用它。这与闭包对象的移动构造是否为 noexcept 是两件不同的事。
十三、Lambda 中的 static 与线程局部变量
auto counter = []() {
static int count = 0; // 所有调用共享
return ++count;
};
counter(); // 1
counter(); // 2
auto per_thread_counter = []() {
thread_local int count = 0; // 每个线程各有一份
return ++count;
};
同一个 lambda 表达式创建的所有闭包对象共享其函数体中的
static局部变量;两个不同的 lambda 表达式即使文本相同,也各自拥有独立的static变量。thread_local变量则在此基础上每个线程各有一份。
⚠️ 上面的
static int count被所有线程共享,多个线程同时执行++count会产生数据竞争,导致未定义行为。若需要每个线程独立计数,可使用thread_local;若需要跨线程共享计数,应使用std::atomic或其他同步机制。
十四、实用技巧汇总
14.1 用 lambda 替代 switch 中的重复代码
auto handle_error = [&](int code) {
log(code);
cleanup();
throw std::runtime_error("Error " + std::to_string(code));
};
switch (status) {
case 1: handle_error(1001); break;
case 2: handle_error(1002); break;
}
14.2 用 lambda 做作用域守卫(Scope Guard,C++17)
#include <mutex>
#include <type_traits>
#include <utility>
template<typename F>
class ScopeGuard {
public:
explicit ScopeGuard(F cleanup) : cleanup_(std::move(cleanup)) {
static_assert(std::is_nothrow_invocable_v<F&>,
"ScopeGuard cleanup must be noexcept");
}
~ScopeGuard() noexcept { cleanup_(); }
ScopeGuard(const ScopeGuard&) = delete;
ScopeGuard& operator=(const ScopeGuard&) = delete;
private:
F cleanup_;
};
template<typename F>
ScopeGuard(F) -> ScopeGuard<F>; // C++17 类模板参数推导
void update(std::mutex& mutex) {
mutex.lock();
ScopeGuard guard{[&mutex]() noexcept {
mutex.unlock(); // guard 析构时执行
}};
// ... 临界区操作
}
lambda 只定义清理操作,必须由 ScopeGuard 这样的 RAII 对象在析构时调用它;单独保存一个 lambda 不会自动执行清理。由于析构函数被声明为 noexcept,清理操作也必须保证不抛出异常,因此示例用 static_assert 在编译期检查这一点。
14.3 用 decltype 获取 lambda 的闭包类型
std::priority_queue 的第三个模板参数必须是比较器的类型:
// 简化声明:仅用于说明三个模板参数,不要复制到代码中
template<
typename T,
typename Container = std::vector<T>,
typename Compare = std::less<typename Container::value_type>
>
class priority_queue;
如果不使用 lambda,通常需要先定义一个有名的比较器类:
struct Item {
double price;
};
struct PriceLess {
bool operator()(const Item& a, const Item& b) const {
return a.price < b.price;
}
};
std::priority_queue<Item, std::vector<Item>, PriceLess> pq;
lambda 可以直接写出比较逻辑,但它的闭包类型是编译器生成的匿名类型,无法手写类型名。decltype(cmp) 用于在编译期取得变量 cmp 的确切类型:
auto cmp = [](const Item& a, const Item& b) {
return a.price < b.price;
};
using Compare = decltype(cmp); // Compare 就是该 lambda 的闭包类型
std::priority_queue<Item, std::vector<Item>, Compare> pq(cmp);
这里不是“简化 decltype 推导”,而是 lambda 省去了单独编写 PriceLess 类的样板代码,decltype 则解决了 lambda 类型无法直接命名的问题。decltype(cmp) 只在编译期获取类型,不会执行 cmp,也不会带来额外的运行时开销。
PriceLess 和 lambda 都在 a.price < b.price 时返回 true,因此上述 priority_queue 的 top() 是价格最大的元素。priority_queue 的比较器方向容易与“谁更小就在前”的直觉混淆,使用时应同时检查 top() 的实际含义。
十五、编译器如何对待 Lambda
auto f = [x](int y) mutable -> int { return x + y; };
编译器大致生成:
class CapturedClosureModel {
int x; // 按值捕获 → 成员变量
public:
explicit CapturedClosureModel(int x) : x(x) {}
int operator()(int y) /* 无 mutable 则加 const */ {
return x + y;
}
};
CapturedClosureModel f(x); // 构造闭包对象
无捕获且无显式对象参数的 lambda 是另一种闭包类型。以下代码用于概念化展示它到函数指针的转换:
// 对应:[](int y) { return y * 2; }
class CapturelessClosureModel {
static int invoke(int y) {
return y * 2;
}
public:
int operator()(int y) const {
return y * 2;
}
using FunctionPtr = int (*)(int);
operator FunctionPtr() const noexcept {
return &invoke;
}
};
CapturelessClosureModel closure;
int (*fp)(int) = closure;
上述类只是便于理解的模型,编译器的真实闭包类型是匿名的,具体实现细节不由这段示意代码规定。这个模型有助于理解:普通按值捕获的副本为什么需要 mutable 才能直接修改、为什么每个 lambda 表达式都产生唯一类型,以及为什么只有无捕获且无显式对象参数的 lambda 才能转换为普通函数指针。
十六、速查表
[捕获] <模板参数> (函数参数) 说明符 noexcept -> 返回类型 requires 约束 { 函数体 }
───── ───────── ──────── ──── ──────── ────── ─────────── ─────────
│ │ │ │ │ │ │
│ │ │ │ │ │ └─ 必须存在
│ │ │ │ │ └─ 可选(C++20)
│ │ │ │ └─ 可选(自动推断)
│ │ │ └─ 可选
│ │ └─ mutable / constexpr / consteval / static 等
│ └─ C++20 模板 lambda 可用
└─ 必须写 [],内部可按需填写捕获项
掌握以上内容,可以覆盖 C++11 至 C++23 中 Lambda 表达式的大部分常见用法。
附录:Lambda 作为算法回调时,参数类型怎么写?
一、核心原则:参数类型由"调用处"决定,不是由你随意定
STL 算法内部是这样调用你传入的 lambda 的:
// std::sort 内部(简化)
if (comp(*iter_a, *iter_b)) { ... }
// ↑ 解引用迭代器得到的值,传给 comp
因此,lambda 的参数必须满足具体算法规定的调用表达式。对许多迭代器算法而言,这意味着 *it(迭代器解引用的结果)必须能合法地转换或绑定到对应参数上;但 std::accumulate 等算法还会传入累加器,需要按其单独规则判断。
并不要求你写的类型和元素类型完全一致。
二、以 std::sort 为例逐一分析
std::vector<int> v = {5, 3, 1, 4, 2};
std::sort(v.begin(), v.end(), /* lambda */);
直接调用通常可以理解为 comp(*it1, *it2),对 std::vector<int> 而言,*it 的类型是 int&。但 std::sort 对比较器还有更完整的要求:它必须也能接受可能带 const 的元素左值,不得通过参数修改元素,并且必须形成严格弱序。
写法 1:完全匹配元素类型(按值)
[](int a, int b) { return a < b; }
✅ 最直观,int 拷贝代价极小。
写法 2:const 引用
[](const int& a, const int& b) { return a < b; }
✅ 合法。*it 返回 int&,可以绑定到 const int&。
对 int 没必要,但对大对象(std::string、自定义 struct)推荐这种写法,避免拷贝。
写法 3:非 const 引用(不满足可移植的比较器要求)
[](int& a, int& b) { return a < b; }
⚠️ 这种写法在某些标准库实现中可能通过编译,但 const int& 无法绑定到 int&,因此它不满足经典算法 BinaryPredicate 对“能接受可能带 const 的元素左值”的完整要求。比较器应优先使用按值或 const T&,并且不得修改比较对象。
写法 4:类型不完全一致,但可隐式转换
std::vector<int> v = {5, 3, 1};
std::sort(v.begin(), v.end(), [](long a, long b) { return a < b; });
// ^^^^ int → long 隐式转换,合法
✅ 合法。int 隐式转换为 long。
std::vector<short> v = {5, 3, 1};
std::sort(v.begin(), v.end(), [](int a, int b) { return a < b; });
// ^^^ short → int 隐式转换,合法
✅ 同理。
写法 5:auto 泛型参数(C++14)
[](auto a, auto b) { return a < b; }
// 等价于:
// template<typename T, typename U>
// bool operator()(T a, U b) const { return a < b; }
✅ 最灵活,参数类型由编译器在调用时推导。
写法 6:参数类型"不兼容"——编译错误
std::vector<std::string> v = {"b", "a"};
std::sort(v.begin(), v.end(), [](int a, int b) { return a < b; });
// ^^^ string 无法转为 int → ❌ 编译失败
写法 7:参数个数不对
std::vector<int> values = {3, 1, 2};
std::sort(values.begin(), values.end(), [](int a) { return a > 0; });
// ^^^^ 只给了1个参数,但 sort 传2个 → ❌
三、自定义类型的情况
struct Student {
std::string name;
int score;
};
std::vector<Student> stu;
以下全部合法:
// ① 按值(会拷贝整个 Student,不推荐)
[](Student a, Student b) { return a.score < b.score; }
// ② const 引用(推荐 ✅)
[](const Student& a, const Student& b) { return a.score < b.score; }
// ③ auto
[](const auto& a, const auto& b) { return a.score < b.score; }
// ④ 只接收需要比较的字段?❌ 不行!
// sort 传的是整个 Student 对象,你不能只写一个 int 参数
[](int score) { return score > 0; } // 函数体合法,但参数个数和类型都不符合 sort 要求
关键:参数个数和"可绑定性"是硬性要求,不是必须类型完全一致。
四、不同算法对 Lambda 参数的要求汇总
4.1 一元操作(1 个参数)
std::for_each(v.begin(), v.end(), [](int x) { std::cout << x; });
std::find_if(v.begin(), v.end(), [](int x) { return x > 3; });
内部调用:func(*it) → lambda 必须接受 1 个参数,类型兼容 *it。
4.2 二元比较(2 个参数)
std::sort(v.begin(), v.end(), [](int a, int b) { return a < b; });
内部调用:comp(*it1, *it2) → 2 个参数,返回可转 bool。
4.3 二元操作(2 个参数,返回非 bool)
// C++11/14/17 可概念化为:acc = op(acc, *it)
// C++20 起为: acc = op(std::move(acc), *it)
int sum = std::accumulate(v.begin(), v.end(), 0,
[](int acc, int x) { return acc + x; });
// ^^^ 第一个参数类型由初始值(0)决定
// ^^^ 第二个参数类型由 *it 决定
⚠️ 注意:acc 的类型跟着初始值走:
// 初始值是 0 (int),即使 vector 是 double,acc 也是 int → 精度丢失!
std::vector<double> vd = {1.5, 2.5};
std::accumulate(vd.begin(), vd.end(), 0, // ← 0 是 int
[](int acc, double x) { return acc + x; });
// lambda 返回 double,但返回值重新赋给 int 类型累加器时小数部分被截断
// 应该写:
std::accumulate(vd.begin(), vd.end(), 0.0, // ← 0.0 是 double
[](double acc, double x) { return acc + x; });
4.4 std::transform(一元版本)
// op(*it) → 写入目标
std::transform(v.begin(), v.end(), v.begin(),
[](int x) { return x * 2; }); // 1个参数
4.5 std::transform(二元版本)
// op(*it1, *it2) → 写入目标
std::transform(a.begin(), a.end(), b.begin(), out.begin(),
[](int x, int y) { return x + y; }); // 2个参数
五、参数传递方式的性能考量
| 元素类型 | 推荐写法 | 原因 |
|---|---|---|
int, char, double 等 | [](int a, int b) | 按值通常很高效,也避免了不必要的间接访问 |
std::string, 大 struct | [](const auto& a, const auto& b) | 避免拷贝 |
需要修改(如 for_each) | [](auto& x) | 非 const 引用 |
| 不确定类型 / 泛型只读回调 | [](const auto& a, const auto& b) | 常用的泛型只读写法 |
六、返回值的要求
sort 的比较器
// 返回类型必须可隐式转换为 bool
[](int a, int b) { return a < b; } // ✅ bool
[](int a, int b) { return a - b; } // ✅ int → bool(但语义错!)
[](int a, int b) { std::cout << a; } // ❌ void 不能转 bool
且必须满足严格弱序(strict weak ordering),否则未定义行为:
// ❌ 不满足严格弱序(a<=b 不是严格弱序)
[](int a, int b) { return a <= b; }
// ✅
[](int a, int b) { return a < b; }
for_each 的返回值
// 返回值被忽略,写什么都行(甚至 void)
std::for_each(v.begin(), v.end(), [](int x) { std::cout << x; });
七、一张图总结参数推导过程
std::sort(v.begin(), v.end(), comp);
│
▼
迭代器类型 RandomIt = vector<int>::iterator
│
▼
*it 的类型 = int&
│
▼
直接调用可来自 comp(*it1, *it2)
│
▼
BinaryPredicate 还要求接受可能带 const 的元素左值
且不得修改元素,必须形成严格弱序
│
┌───────────┼───────────────────────┐
▼ ▼ ▼
(int a, (const int& a, (auto a,
int b) const int& b) auto b)
✅ ✅ ✅
(long a, (int& a, (int a)
long b) int& b) ❌ 参数个数错
✅(隐式转) ❌(不接受 const) ❌
八、结论
Lambda 的参数不要求与容器元素类型完全一致,只要求:
- 参数个数与具体算法规定的调用表达式一致;
- 每个参数类型能接受算法传入的对应实参:迭代器实参通常来自
*it,std::accumulate的第一个实参则来自累加器; - 返回值和调用语义满足算法要求;例如,排序比较器的返回值必须可测试为真值,比较关系必须形成严格弱序,且不得修改比较对象。
对比较器或只读算法回调,拿不准具体元素类型时,const auto& 通常是安全且通用的选择;需要修改元素或接受右值时,应按算法的实际调用规则选择其他参数形式。
更多推荐
所有评论(0)