20. 高阶函数:返回 Lambda 的函数,闭包工厂模式

说实话,刚接触 C++ Lambda 那会儿,我总觉得它就是个语法糖——写个匿名函数,省得再单独定义个仿函数。直到我在项目中遇到一个场景:需要根据不同的配置参数,生成一系列行为相似但细节不同的处理函数。那时候我才意识到,Lambda 真正的威力,在于它可以被「生产」出来。

今天我们就聊聊这个——返回 Lambda 的函数,也就是所谓的「高阶函数」。说白了,就是函数也能当工厂,专门生产别的函数。

20.1 什么是高阶函数?

高阶函数这个概念,在函数式编程里很常见。它要么接受函数作为参数,要么返回一个函数。C++ 里,我们主要关注后者——返回一个可调用对象

你想想看,如果一段逻辑只有参数不同,其他都一样,那何必每次都手写一遍?写个工厂函数,把变化的部分作为参数传进去,让它帮你生成对应的 Lambda,多清爽。

核心思想: 高阶函数 = 函数工厂。输入配置,输出行为。

20.2 闭包工厂模式

闭包工厂,说白了就是「捕获外部变量并返回 Lambda 的函数」。每次调用工厂,都会生成一个独立的闭包,闭包里保存了当时捕获的变量副本。

我在项目中遇到过这样一个需求:需要为不同的传感器生成不同的数据过滤函数。每个传感器的阈值不一样,但过滤逻辑完全一样。用闭包工厂,几行代码就搞定了。

#include <iostream>
#include <functional>
#include <vector>

// 闭包工厂:生成一个比较函数
auto makeComparator(int threshold) {
    // 返回的 Lambda 捕获了 threshold
    return [threshold](int value) -> bool {
        return value > threshold;
    };
}

int main() {
    // 生产两个不同的比较器
    auto cmp1 = makeComparator(10);
    auto cmp2 = makeComparator(50);

    std::cout << cmp1(20) << std::endl;  // 1 (20 > 10)
    std::cout << cmp2(20) << std::endl;  // 0 (20 < 50)

    return 0;
}

你看,makeComparator 就是个工厂。每次调用,它都生成一个全新的闭包,闭包里保存着当时的 threshold 值。两个闭包互不干扰。

20.3 捕获方式的影响

这里有个坑,我曾经踩过。捕获方式不同,闭包的行为可能天差地别。

捕获方式 行为 典型场景
按值捕获 [=] 拷贝一份,闭包独立 工厂函数返回后,变量已销毁
按引用捕获 [&] 引用外部变量,需保证生命周期 变量生命周期比闭包长
移动捕获 [x = std::move(v)] 转移所有权,避免拷贝 管理独占资源(如 unique_ptr)

我曾经犯过的错: 在工厂函数里用 [&] 捕获局部变量,结果函数返回后,局部变量销毁了,闭包变成了悬空引用。程序跑起来时好时坏,查了半天才发现是引用捕获的问题。

所以我的建议是:闭包工厂里,默认用按值捕获。除非你非常确定引用对象的生命周期,否则别冒险。

20.4 返回 std::function 还是 auto?

嗯,这里要注意。Lambda 的类型是匿名的,你没法直接写出来。所以返回时有两种选择:

  • auto 推导返回类型:C++14 开始支持,简洁高效。但只能在函数定义里用,不能用于头文件声明。
  • std::function 包装:类型擦除,可以写在头文件里。但会有轻微的性能开销。
// 方式一:auto 推导(推荐,性能好)
auto makeAdder(int x) {
    return [x](int y) { return x + y; };
}

// 方式二:std::function(可声明,有开销)
std::function<int(int)> makeAdder2(int x) {
    return [x](int y) { return x + y; };
}

我个人习惯:内部实现用 auto,对外接口用 std::function。这样既保证了性能,又保持了接口的清晰。

20.5 实战:策略模式 + 闭包工厂

闭包工厂最实用的场景,就是配合策略模式。我在一个数据处理框架里就这么用过。

#include <iostream>
#include <functional>
#include <vector>
#include <string>

// 策略工厂:根据策略名称生成对应的处理函数
auto createProcessor(const std::string& strategy) {
    if (strategy == "double") {
        return [](int x) { return x * 2; };
    } else if (strategy == "square") {
        return [](int x) { return x * x; };
    } else {
        // 默认策略:原样返回
        return [](int x) { return x; };
    }
}

int main() {
    auto proc = createProcessor("square");
    std::vector<int> data = {1, 2, 3, 4, 5};

    for (auto& v : data) {
        v = proc(v);
    }

    for (auto v : data) {
        std::cout << v << " ";  // 1 4 9 16 25
    }

    return 0;
}

你看,策略的选择被封装在工厂函数里。调用方只需要告诉工厂「我要什么策略」,工厂就给你一个对应的 Lambda。代码干净,扩展也方便——加个新策略,只要在工厂里加个分支就行。

20.6 闭包工厂的陷阱

最后聊几个我踩过的坑:

  • 捕获大型对象:按值捕获会拷贝整个对象。如果对象很大(比如一个大容器),性能会受影响。可以考虑用 std::shared_ptr 共享所有权。
  • 递归 Lambda:Lambda 默认不能递归,因为捕获时自身还没定义。可以用 std::function 包装,或者用 Y 组合子(不过后者太学术,不推荐)。
  • 默认捕获 [=] 的陷阱:在成员函数里用 [=],会隐式捕获 this 指针。如果对象被销毁,闭包就悬空了。建议显式捕获 this 或者按值捕获成员变量。

小技巧: 如果闭包工厂返回的 Lambda 需要修改捕获的变量,记得加上 mutable 关键字。否则编译器会报错。

auto makeCounter() {
    int count = 0;
    return [count]() mutable { return ++count; };
    // 注意:mutable 允许修改按值捕获的 count
}

20.7 知识体系图

下面这张图,帮你理清本章的核心脉络:

高阶函数与闭包工厂模式 闭包工厂 返回 Lambda 的函数 捕获方式:值/引用/移动 auto vs std::function 策略模式实战 生命周期管理 性能与接口权衡 核心:用函数生产函数,用闭包封装状态

闭包工厂模式,说白了就是「用函数生产函数,用闭包封装状态」。它让代码更灵活,也更可复用。你在项目中遇到「同一套逻辑,不同参数」的场景时,不妨试试这个模式。


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