C++ Lambda 表达式


一、基本语法结构

[捕获列表]<模板参数>(函数参数) 说明符 noexcept -> 返回类型 requires 约束 { 函数体 }

其中 [] 和函数体必须存在;模板参数(C++20)、函数参数、说明符、noexcept、尾置返回类型和约束等都是按需使用的可选部分。常见说明符包括 mutableconstexpr(C++17)、consteval(C++20)和 static(C++23)。static lambda 必须无捕获,且不能与 mutable 同时使用;constexprconsteval 也不能同时出现。最简形式为:

[]{};              // 合法:无捕获、无参数、无返回值、空函数体
[](){};            // 等价写法

二、捕获列表(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 不能使用 mutablestatic 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);
}

函数体中的 fx 都是左值,因此约束应检查 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 的存储与传递方式对比

方式写法开销适用场景
autoauto f = []{...};保留具体闭包类型,便于优化局部使用
模板参数template<typename F>无类型擦除,通常便于内联函数接收回调
std::functionstd::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_queuetop() 是价格最大的元素。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 的参数不要求与容器元素类型完全一致,只要求:

  1. 参数个数与具体算法规定的调用表达式一致;
  2. 每个参数类型能接受算法传入的对应实参:迭代器实参通常来自 *itstd::accumulate 的第一个实参则来自累加器;
  3. 返回值和调用语义满足算法要求;例如,排序比较器的返回值必须可测试为真值,比较关系必须形成严格弱序,且不得修改比较对象。

对比较器或只读算法回调,拿不准具体元素类型时,const auto& 通常是安全且通用的选择;需要修改元素或接受右值时,应按算法的实际调用规则选择其他参数形式。

更多推荐