6. Lambda的存储与传递:std::function、auto存储、作为参数传递

Lambda表达式写出来容易,但怎么存、怎么传,才是真正考验功底的地方。我见过不少同事,Lambda写得飞起,结果一涉及到存储和传递就翻车。说白了,Lambda是个临时对象,你得知道怎么把它「抓住」、怎么把它「递出去」。

6.1 用auto捕获Lambda——最直观的方式

C++11开始,auto可以推导Lambda的类型。每个Lambda都有独一无二的类型,编译器会为它生成一个匿名函数对象。用auto来存,最省事。

auto add = [](int a, int b) { return a + b; };
std::cout << add(3, 4) << std::endl; // 输出7

嗯,这里要注意:auto存储的Lambda,类型是唯一的、不可写的。你不能把另一个Lambda赋给它,哪怕签名一模一样。

auto f = [](int x) { return x * 2; };
// auto f2 = [](int x) { return x * 3; };
// f = f2; // 编译错误!类型不匹配

我在项目中遇到过这种场景:写一个排序比较器,用auto存Lambda,然后传给std::sort。当时觉得挺方便,后来需求变了,需要动态切换比较逻辑……auto这条路就走不通了。

我的习惯:如果Lambda只在当前作用域用一次,用auto没问题。需要存起来反复用、或者作为成员变量,就得换std::function。

6.2 std::function——万能容器

std::function是一个多态函数包装器。它可以存储任何可调用对象:Lambda、函数指针、函数对象……只要签名匹配就行。

#include <functional>

std::function<int(int, int)> func;

func = [](int a, int b) { return a + b; };
std::cout << func(10, 20) << std::endl; // 30

func = [](int a, int b) { return a * b; };
std::cout << func(10, 20) << std::endl; // 200

你看,同一个func可以换不同的Lambda。这就是std::function的价值——类型擦除。它把具体类型藏起来了,只暴露统一的调用接口。

核心区别:
  • auto:零开销,类型唯一,不能赋值给不同Lambda
  • std::function:有少量开销(虚函数调用、可能堆分配),支持赋值和重新绑定

我曾经在一个回调系统里用过std::function。一开始觉得性能没问题,后来压测发现高频调用时开销不小。优化方案是:能用模板参数就别用std::function,只有在需要类型擦除时才用它。

6.3 作为参数传递——三种常见方式

把Lambda传给函数,有三种主流写法。我按推荐程度排个序:

方式 代码示例 适用场景 开销
模板参数 template<typename F> void call(F f) 通用、高性能 零开销
std::function void call(std::function<void()> f) 类型擦除、存储回调 中等
auto参数(C++20) void call(auto f) 简洁、类似模板 零开销

模板参数是最灵活的。你想想看,编译器会为每个Lambda生成特化版本,没有虚函数开销,还能内联。

template <typename Func>
void repeat(int n, Func f) {
    for (int i = 0; i < n; ++i) {
        f(i);
    }
}

// 调用
repeat(5, [](int i) { std::cout << i << " "; });

std::function作为参数,适合需要把回调存起来的场景。比如事件系统、异步任务队列。

void register_handler(std::function<void(int)> handler) {
    // 把handler存到容器里
    handlers.push_back(std::move(handler));
}

C++20的auto参数是语法糖,本质还是模板。我个人觉得可读性更好,但团队里有人觉得隐式模板不够明确。看你们项目风格吧。

避坑指南:我曾经在回调里捕获了引用,结果对象销毁后回调才执行……程序直接崩溃。记住:Lambda捕获引用时,你得保证被引用对象的生命周期比Lambda长。否则就用值捕获,或者用std::shared_ptr。

6.4 存储Lambda的实战场景

实际项目中,Lambda的存储和传递往往不是孤立的。我整理了几个常见模式:

6.4.1 回调容器

class Button {
    std::vector<std::function<void()>> click_handlers_;
public:
    void on_click(std::function<void()> handler) {
        click_handlers_.push_back(std::move(handler));
    }
    void click() {
        for (auto& h : click_handlers_) {
            h();
        }
    }
};

这里必须用std::function,因为容器需要统一类型。auto做不到。

6.4.2 延迟执行

std::function<int()> create_adder(int x) {
    return [x](int y) { return x + y; };
}

auto add5 = create_adder(5);
std::cout << add5(10) << std::endl; // 15

返回Lambda时,返回值类型必须明确。std::function是自然的选择。auto推导在返回类型上不好用——除非你用decltype,但太啰嗦了。

6.4.3 策略模式

class Sorter {
    std::function<bool(int, int)> comp_;
public:
    void set_comparator(std::function<bool(int, int)> comp) {
        comp_ = std::move(comp);
    }
    void sort(std::vector<int>& data) {
        std::sort(data.begin(), data.end(), comp_);
    }
};

运行时切换排序策略,std::function是标配。用模板的话,你得把Sorter也变成模板类,灵活性就差了。

6.5 性能取舍——我的一些经验

很多人担心std::function的性能。说实话,大部分场景下那点开销可以忽略。但如果你写的是高性能计算、游戏引擎、高频交易系统,就得认真对待了。

我做过一个测试:调用1000万次空Lambda,模板版本比std::function快大约2-3倍。原因在于std::function内部有虚函数调用,而且可能涉及小对象优化(SBO)或堆分配。

我的建议是:

  • 默认用模板参数,除非你需要类型擦除
  • 需要存储回调时,用std::function,但注意捕获的变量不要太大(超过SBO缓冲区会触发堆分配)
  • 如果Lambda捕获了很多变量,考虑用std::shared_ptr包装数据,或者改用函数对象
小技巧:std::function的SBO大小因实现而异。gcc的libstdc++通常是16字节,clang的libc++是32字节。捕获少量int或指针时,不会触发堆分配。

6.6 本章知识体系

下面这张图梳理了Lambda存储与传递的核心脉络:

Lambda存储与传递 auto 存储 std::function 作为参数传递 类型唯一,零开销 不能赋值给不同Lambda 类型擦除,可重新绑定 少量开销,支持SBO 适合存储回调、延迟执行 模板参数(推荐) std::function参数 auto参数(C++20)

说白了,选择哪种方式取决于你的需求:要性能选auto或模板,要灵活选std::function。没有银弹,只有权衡。我在项目中通常这样决策:如果Lambda只在局部用,auto搞定;如果需要跨函数传递或存储,先试试模板,不行再上std::function。

记住一点:Lambda本身是轻量的,但捕获的变量和存储方式会影响性能。多想想生命周期,多想想类型擦除的代价。嗯,这些坑我都踩过,希望你能少走弯路。

公众号:蓝海资料掘金营,微信 deep3321