Lambda表达式

C++98/03 时代,如果你想给 std::sort、std::for_each、std::find_if 传一个自定义操作,只能写全局函数或写一个 functor 类,要定义 class、重载 operator(),写一堆无用代码。 C++委员会看到 Python、C#、Java 8 的匿名函数已经普及,就决定在 C++11 加 Lambda。 核心目标只有两个:就地写小函数,让代码逻辑更连贯 + 必须零开销,生成的机器码跟手写 functor 一模一样,不能变慢。这就是现代 C++“函数式编程”风格的起点。

Lambda表达式的语法结构:

[捕获列表](参数列表) mutable -> 返回类型 { 函数体 }

Lambda 根本不是函数,而是编译器在背后偷偷生成的一个独一无二的类并创建其对象,类型叫闭包类型 closure type,举个例子:

int factor = 10;
auto lam = [factor](int x) { return x * factor; };

编译器实际生成的代码大致是这样的:

struct __lambda_12345 {          // 每个 lambda 都有唯一内部名字
    int factor;                  // 捕获的变量变成成员
    __lambda_12345(int f) : factor(f) {}   // 构造函数
    int operator()(int x) const {          // 重载调用运算符
        return x * factor;
    }
};
auto lam = __lambda_12345(factor);   // 实例化
  • 捕获列表 [ ] 决定这个类有哪些成员变量 + 构造函数。

  • 每个 lambda 在不同位置生成的类都不一样,所以必须用 auto 接收。

  • 这就是“零开销”的秘密:最后汇编和自己手写一个 functor 类完全相同,不会多一条指令。

[捕获列表](参数列表) mutable -> 返回类型 { 函数体 }

什么时候必须用 -> 尾置返回类型?

  • C++11 环境下的复杂 Lambda:包含多个 return 语句或返回类型不明显时。

    // C++11:如果有多个 return 或类型复杂,必须显式指定
    auto func = [](int x) -> double {
        if (x > 0) {
            return x * 1.5; // 返回 double
        } else {
            return x * 2.0; // 也返回 double
        }
    };
  • 需要显式指定返回类型的场景:即使编译器能推导,你也想强制指定返回类型(如强制返回 double 而非 float

  • C++14 增强了返回类型推导能力,普通 Lambda 直接用 auto 即可

捕获列表:

  • [] 不捕获任何外部变量

  • [=] 值捕获所有(拷贝一份,安全)

  • [&] 引用捕获所有(最常用,性能最好)

  • [x] 只值捕获 x

  • [&x] 只引用捕获 x

  • [=, &x] 混合捕获(所有值捕获,但 x 用引用)

  • [this] 捕获当前对象的 this(类成员函数里常用)

  • C++17 :[self = *this](值捕获整个对象,避免悬挂引用)

mutable:值捕获默认是 const 的,加了才能在 lambda 里修改拷贝。因为 lambda 的 operator() 默认是 const。

C++11 的 Lambda 已经让就地定义小函数变得非常简洁,但捕获列表仍然存在局限性:只能直接捕获外部已有变量,无法在捕获列表中直接“初始化”一个新变量、进行类型转换、或者安全地移动资源(例如 std::move 一个 unique_ptr)。

C++14 引入初始化捕获(init-capture),目的是让 Lambda 捕获更加灵活、表达力更强,尤其在需要移动语义、创建临时对象、或者避免外部变量生命周期问题时,能写出更安全、更简洁的代码,同时保持零开销抽象。

Lambda 初始化捕获

C++14 允许在捕获列表中使用 var = expr 形式,在 Lambda 构造时直接初始化一个新成员变量,解决移动资源、临时对象、生命周期管理等实际痛点。

  • 值捕获形式:[var = expr] → 拷贝/移动构造

  • 引用捕获形式:[var = std::ref(expr)] → 引用

  • 支持 std::move、std::make_unique 等任意表达式


// 安全移动 unique_ptr
///////////////////////////////////////////////////////
// C++11 老写法(必须用引用,生命周期容易出错)
std::unique_ptr<int> p = std::make_unique<int>(42);
auto lam = [&p]() { return *p; };   // p 必须活得比 lambda 长

// C++14 新写法(初始化捕获 + move)
auto lam = [ptr = std::move(p)]() mutable {   // ptr 是 lambda 内部的新成员
    return *ptr;
};
// p 现在已经为空,资源被安全移动进 lambda 内部
////////////////////////////////////////////////////////


// 在捕获列表中直接创建临时对象
////////////////////////////////////////////////////////
// C++11 老写法(必须在外部创建临时变量)
std::string temp = "hello";
auto lam = [temp = std::move(temp)]() { return temp + " world"; };

// C++14 新写法(直接在捕获列表初始化)
auto lam = [temp = std::string("hello")]() { return temp + " world"; };
///////////////////////////////////////////////////////



// 给捕获变量起新名字或进行转换
//////////////////////////////////////////////////////
int value = 42;
auto lam = [doubled = value * 2]() { return doubled; };   // doubled 是新名字,值为 84

// 更复杂转换
auto lam = [upper = std::string("hello")]() { 
    std::transform(upper.begin(), upper.end(), upper.begin(), ::toupper);
    return upper;
};
/////////////////////////////////////////////////////

常见坑:

  • 初始化捕获的变量只在 lambda 内部可见,外部无法访问。

  • 使用 std::move 后,外部原变量进入“有效但未指定”状态,不能再使用。

  • 如果 lambda 需要修改捕获的变量,必须加上 mutable 关键字(。

  • 生命周期:初始化捕获的变量生命周期和 lambda 本身绑定,不会悬挂引用。

泛型 Lambda

C++14 允许 Lambda 的参数使用 auto,这让 Lambda 瞬间拥有了模板的能力:

// C++14:一个 Lambda 走天下
auto add = [](auto a, auto b) {
    return a + b;
};

add(1, 2);                  // int
add(3.14, 2.71);            // double
add(std::string("a"), "b"); // std::string

Lambda的坑:引用捕获后 lambda 活得比外部变量久 ,会导致悬挂引用

auto bad() {
    int x = 42;
    return [&]() { return x; };   // 返回后 x 已经销毁!
}

正确做法:用 [=] 值捕获,或 C++14 的 [x = x]

常见坑:

  • 泛型 Lambda 的模板实例化是延迟的:泛型 Lambda 的 operator() 模板只有在调用时才会实例化,若 Lambda 内部有语法错误,只要不调用就不会触发编译报错,调试时需注意。

  • 显式模板参数必须在调用时指定:带模板参数列表的 Lambda,调用时必须显式指定模板参数,或让编译器完全推导,无法部分指定。

  • 避免泛型 Lambda 过度使用导致的编译期膨胀:每一种不同的参数类型组合,都会生成一个新的模板特化版本,过度使用会导致编译期变长、目标文件体积变大。

  • Lambda 底层是怎么实现的?为什么零开销?

编译器生成一个临时类,重载 operator(),捕获变成成员变量,和手写 functor 完全一样。

  • [=] 和 [&] 区别?什么时候用哪个?

[=] 拷贝(安全但有拷贝开销),[&] 引用,但要注意生命周期。

  • C++14 初始化捕获和 C++11 普通捕获的本质区别是什么?

普通捕获是直接绑定外部已有变量;初始化捕获是在捕获列表里创建一个全新的成员变量并初始化,常用于移动资源或创建临时对象。

  • 什么时候必须使用初始化捕获?

需要移动资源进 lambda(unique_ptr、string 等)、想在 lambda 内部创建一个独立副本、或者需要给捕获的变量起新名字/进行转换时。

  • 初始化捕获的变量是值捕获还是引用捕获?

于你写的形式,[var = expr] 是值,[var = std::ref(expr)] 是引用。

  • C++14 泛型 lambda 有什么用?

可以写一个 lambda 支持任意类型(int、double、string 等)

一些建议:

  • 短 lambda 用 [&]

  • 要返回或传给别处的 lambda 用 [=]

  • 类成员函数里捕获 *this 强烈推荐 [self = *this](C++17)

  • 初始化捕获 + std::move 是 Lambda 中使用移动语义的标准写法,大量出现在回调函数、线程任务、STL 算法中


函数包装器相关特性

C++11 希望让函数式编程变得简单实用。Lambda 解决了“就地写小函数”的问题,但实际项目中经常需要存储、传递、复制一个“可调用对象”,比如 Lambda、普通函数、成员函数等。 为了解决这个问题,C++11 同时引入了 std::function、std::bind、std::ref、std::mem_fn 等工具,形成了一套完整的“函数对象包装”体系,让 Lambda 真正能被用在回调、事件、任务队列等场景中。

std::function

设计原因: 需要一种统一的、可存储、可复制的类型来保存任意可调用对象(Lambda、普通函数、成员函数、functor)。以前只能用函数指针,无法捕获状态或手写类,非常麻烦。

底层原理: std::function<R(Args...)> 是类型擦除的包装器。内部用虚表 + 小对象优化(,小对象直接放在栈上,大对象才分配堆内存。

代码示例:

#include <functional>

// 1. 存储 Lambda
std::function<void(int)> callback = [](int x) {
    std::cout << "收到: " << x << '\n';
};

// 2. 存储普通函数
void my_func(int a) { ... }
std::function<void(int)> f = my_func;

// 3. 事件系统常用写法
class Button {
    std::function<void()> on_click;
public:
    void set_handler(std::function<void()> h) {
        on_click = std::move(h);   // 推荐用 move
    }
    void click() { if (on_click) on_click(); }
};

常见坑

  • std::function 本身可拷贝,容易产生意外拷贝开销。

  • 不要在性能热路径上大量使用,而是优先模板或直接 Lambda。

  • 存储 Lambda 时一定要 std::move。

std::bind

设计原因: 想把函数的部分参数“绑定”成固定值,生成一个新的可调用对象,常用于适配旧接口。

底层原理:返回一个可调用对象,内部保存被绑定的参数和占位符(std::placeholders::_1 等)。

实际例子:

void handler(int a, std::string b, double c) { ... }

// 绑定后两个参数
auto bound = std::bind(handler, 42, std::placeholders::_1, 3.14);
bound("hello");   // 实际调用 handler(42, "hello", 3.14)

与 Lambda 对比,在项目中更推荐用 Lambda,std::bind 主要用于兼容旧代码或需要绑定成员函数时。

需要注意的是,哪么绑定的函数的参数是引用类型,std::bind 默认是 “值拷贝” 的。

#include <iostream>
#include <functional>  // 要用到 std::bind

void add_one(int& n) { n++; }

int main() {
    int x = 10;

    // 不用 ref:bind 会偷偷把 x 拷贝一份,传给 add_one
    auto func = std::bind(add_one, x); 
    func();  // 调用函数

    std::cout << x << std::endl;  // 输出 10!原变量没改!
}

std::ref / std::cref + std::reference_wrapper

设计原因:C++11 的 std::bindstd::function 以及 STL 算法,默认全是拷贝传参—— 你想传个引用进去改原对象?直接传 & 没用,它会拷贝成副本,再传引用进去。而std::ref 提供一种安全的引用包装,可以让 std::function 和 std::bind 正确处理引用。

std::reference_wrapper<T> = 能拷贝、能赋值的 “轻量引用壳子”

  • std::ref(x) → 包普通左值引用 T&

  • std::cref(x) → 包只读引用 const T&

底层原理:

std::reference_wrapper<T> = 能拷贝、能赋值的 “轻量引用壳子”,是一个 **“引用的包装器”**,本质是一个类模板,里面存了一个指向原对象的指针。手动写 std::reference_wrapper<T> 太麻烦,C++11 提供了两个辅助函数:

  • std::ref(x):生成一个 std::reference_wrapper<T>,包装 x 的非 const 引用;

  • std::cref(x):生成一个 std::reference_wrapper<const T>,包装 x 的const 引用。

实际例子

#include <iostream>
#include <functional>

int main() {
    int x = 10;

    // 用 std::ref 包装 x,Lambda 里捕获的是 std::reference_wrapper
    auto lambda = [x_ref = std::ref(x)]() mutable {
        // x_ref 是 std::reference_wrapper<int>,可以隐式转换为 int&
        x_ref.get() = 20;  // 或者直接写 x_ref = 20(隐式转换)
        std::cout << "Lambda 内部 x:" << x_ref << '\n';
    };

    lambda();          // 输出:Lambda 内部 x:20
    std::cout << "外部 x:" << x << '\n';  // 输出:外部 x:20(原对象改了!)

    // 用 std::function 存 Lambda,也能正确保持引用
    std::function<void()> func = lambda;
    func();            // 输出:Lambda 内部 x:20(继续修改原对象)
    std::cout << "外部 x:" << x << '\n';  // 输出:外部 x:20
}

以std::bind部分的示例代码为例进行修改:

#include <iostream>
#include <functional>

// 一个接收引用参数的函数
void add_one(int& n) {
    n++;
}

int main() {
    int x = 10;

    // 错误写法:不用 std::ref,bind 会拷贝 x,add_one 改的是副本
    auto bad_bind = std::bind(add_one, x);
    bad_bind();
    std::cout << "x(不用 ref):" << x << '\n';  // 输出:10(没改!)

    // 正确写法:用 std::ref 包装 x,bind 会传递引用
    auto good_bind = std::bind(add_one, std::ref(x));
    good_bind();
    std::cout << "x(用 ref):" << x << '\n';  // 输出:11(改了!)
}

为什么改了?

std::ref(x) 会生成一个叫 std::reference_wrapper 的小东西 ,它本质是个 “壳子”,里面包着 x 的引用。std::bind 看到这个壳子,就知道 “哦,这是要传引用”,于是就不会拷贝 x 了,直接让 add_one 操作原变量。

场景做法
std::bind 传普通引用参数变量外面套 std::ref()
std::bind 传只读引用参数变量外面套 std::cref()
其他时候(比如普通函数调用)直接用 & 就行,不用 ref

需要注意的坑,则是生命周期。

std::ref 只是 “包装引用”,不会延长原对象的生命周期,如果原对象销毁了,std::reference_wrapper 也会变成悬空的,和引用捕获一样危险:

#include <iostream>
#include <functional>

std::function<void()> get_bad_func() {
    int x = 10;
    // 用 std::ref 包装局部变量 x,函数返回后 x 销毁
    return std::bind([](int& n) { std::cout << n << '\n'; }, std::ref(x));
}

int main() {
    auto bad_func = get_bad_func();
    bad_func();  // 危险!访问悬空引用
}

所以被 std::ref 包装的对象,必须在使用它的 Lambda/function/bind 存在期间保持有效。

QA:

  • std::function 底层是怎么实现的?为什么有性能损失?

类型擦除 + 小对象优化。大对象需要堆分配和虚表调用。

  • 什么时候必须用 std::function?

需要把可调用对象存起来、作为成员变量、传给回调、事件系统、任务队列等其他函数时。

  • std::bind 和 Lambda 哪个更好?

现代 C++ 推荐 Lambda,代码更清晰、可读性更高。std::bind 主要用于需要绑定成员函数或部分参数固定的场景。

  • 为什么需要 std::ref?

让 std::function、std::bind 等能正确存储引用,而不是拷贝。

更多推荐