CC++ & Algorithm

std::function与std::bind函数适配器

极难3
语言版本:通用
概述:std::function像一个万能函数盒子,可以装任何可调用对象;std::bind像是胶水,能把部分参数固定住,组合出新的函数。

万能函数盒与参数胶水:std::function 和 std::bind 让你灵活操控函数

你有没有遇到过这样的情形:写了一个通用函数,但有时候需要提前固定几个参数,剩下的以后再填?或者想把不同的“可执行代码”(普通函数、Lambda、类里的方法)放到同一个数组里,统一调用?C++ 中 std::functionstd::bind 就是帮我们解决这类问题的工具。

  • std::function 就像一个“万能函数盒”——无论里面装的是手写指令(Lambda)、菜谱(普通函数)还是自动炒菜机(仿函数),只要它能被调用,就能放进这个盒子里,并且用统一的方式“启动”(调用)。
  • std::bind 就像“参数胶水”或“部分预填表”——你有一张需要两个参数的函数“申请表”,但你现在只能填其中一个,另一个位置先贴上一张占位符贴纸(比如“第1个输入”),等以后真正使用的时候再补上。它甚至还能调换参数的顺序!

下面我们一步步拆解,让这两个“秘密武器”变得 easy 又有趣。


一、std::function:一个可以装下任何可调用对象的万能盒子

1.1 它是什么?为什么要用它?

想象一下,你正在设计一个“游戏技能系统”。每个英雄的“技能”可能是一个普通函数(比如 void fireball()),也可能是一个 Lambda(比如 []{ activateShield(); }),甚至是一个类的成员函数(比如 warrior.doubleAttack())。你希望把这些不同类型的技能都放在一个 vector 里,然后循环调用它们。没有 std::function 的话,每种技能的类型都不一样,根本放不到同一个容器里。而 std::function 通过类型擦除技术,让所有可以调用的东西都包装成同一种类型,只要它们的签名(返回值类型和参数类型)相同。

1.2 基本用法:存进去,取出来,用起来

std::function 定义在 <functional> 头文件中。它的模板参数是一个函数签名:返回值类型(参数类型1, 参数类型2, ...)

#include <functional>

// 声明一个可以装“接受两个int、返回int”的可调用对象的盒子
std::function<int(int,int)> myBox;

给盒子赋值很简单,可以装任何满足签名的东西:

// 1. 装一个 Lambda
myBox = [](int a, int b) { return a + b; };
int result = myBox(3, 4);      // 调用这个 Lambda,result = 7

// 2. 装一个普通函数
int multiplyFunc(int x, int y) { return x * y; }
myBox = multiplyFunc;          // 注意:普通函数名会自动转换成函数指针
result = myBox(5, 6);          // 30

// 3. 装一个仿函数(重载了 operator() 的类对象)
struct Multiplier {
    int operator()(int a, int b) { return a * b * 2; }
};
myBox = Multiplier();
result = myBox(2, 3);          // 12

1.3 生活中更贴近的例子:老师检查作业

老师有一个名册(vector),里面可以放不同同学的“检查作业方法”。小明的检查法是看有没有写名字,小红的检查法是看计算对不对。这些方法可以包装成 std::function<bool(HomeWork)>,然后老师就可以统一调用:for (auto& check : checkList) { check(homework); }。这样如果以后增加新的检查规则,只要往容器里加一个新的可调用对象就行了,不需要改循环代码。

1.4 常见错误与提醒

  • 忘记包含头文件:没有 #include <functional> 就不能用 std::function
  • 签名不匹配:不能把一个返回 void、参数为 int 的函数赋给 std::function<int(double)>
  • 性能小提醒std::function 内部用了一些动态内存和虚函数调用(类型擦除),比直接调用原始函数慢一点点。但在绝大多数日常代码中(比如事件回调、界面按钮、配置中心)完全够用,不需要担心。只有像每秒几百万次调用的热循环中才需要考虑更轻量的方式(比如模板或函数指针)。

1.5 使用场景一览

场景例子
事件回调按钮点击、网络请求完成
策略模式不同排序算法、不同加密方式
线程池任务队列每个任务都是一个 std::function<void()>
结合 std::bind 存储绑定的函数后面马上讲到

二、std::bind:像胶水一样固定参数,让函数更灵活

2.1 生活中的类比:配奶茶

你有一个配方函数 makeMilkTea(base, sugarLevel, iceLevel)。你发现最喜欢吃“珍珠奶茶、五分糖、去冰”。每次都要输入这三个参数很麻烦。你可以用 std::bind 把前两个参数固定(“珍珠基底”和“五分糖”),第三个参数留为占位符,这样以后点单的时候只需要说“做一杯” -> 自动得到固定口味。

2.2 基本语法

std::bind 也定义在 <functional> 中,它返回一个可调用对象,可以指定函数的某些参数为固定值,其余参数用占位符 _1_2_3……表示“将来调用时提供的第 N 个参数”。占位符定义在 std::placeholders 命名空间。

#include <functional>
using namespace std::placeholders;   // 方便使用 _1, _2, ...

auto newFunc = std::bind(原始函数, 固定值或占位符, ...);

2.3 例子:固定前几个参数

#include <iostream>
#include <functional>
using namespace std::placeholders;

int add(int a, int b, int c) {
    return a + b + c;
}

int main() {
    // 固定 a=10, b=20, c 由调用时提供
    auto add10_and_20 = std::bind(add, 10, 20, _1);
    std::cout << add10_and_20(5) << std::endl;   // 10+20+5 = 35

    // 固定 a=1, c=100, b 占位
    auto add1_and_100 = std::bind(add, 1, _1, 100);
    std::cout << add1_and_100(50) << std::endl;  // 1+50+100 = 151

    return 0;
}

2.4 调整参数顺序

占位符可以任意安排位置,甚至重复使用:

// 假设有个函数: void show(string name, int score)
void show(string name, int score) {
    cout << name << " 考了 " << score << " 分" << endl;
}

// 我们想创建一个新函数,交换参数顺序:先给分数,再给名字
auto show_reversed = std::bind(show, _2, _1);
show_reversed(95, "小明");   // 输出: 小明 考了 95 分

注意:_2 对应调用 show_reversed 时的第二个参数("小明"),_1 对应第一个参数(95),所以传递给 show 的顺序是 ("小明", 95)

2.5 绑定成员函数

成员函数有一个隐藏的 this 指针参数。你需要用 std::bind 把对象地址或者对象的引用/指针传过去:

struct Student {
    void printScore(int score) {
        cout << "分数: " << score << endl;
    }
};

Student stu;
// 绑定成员函数,第一个固定参数是对象指针 &stu,第二个参数留作占位
auto printStuScore = std::bind(&Student::printScore, &stu, _1);
printStuScore(88);   // 调用 stu.printScore(88)

对于静态成员函数,不需要对象,直接像普通函数一样绑定即可。

2.6 引用绑定:注意默认是拷贝

std::bind 默认对绑定的参数按值传递,即创建一份拷贝。如果希望修改外部变量,需要用 std::refstd::cref 包装:

int x = 10;
auto addToX = std::bind([](int& a, int b) { a += b; }, std::ref(x), _1);
addToX(5);
std::cout << x;   // 15  (x 被成功修改)

如果忘了 std::refx 会拷贝一份,内部修改只影响拷贝,原 x 不变。这是新手最容易踩的坑!

2.7 什么时候用 bind,什么时候用 Lambda?

C++11 之后,Lambda 表达式通常更直观、更灵活,可以捕获变量、写复杂逻辑。大多数情况下,优先用 Lambda。但是 std::bind 在某些场景仍然有优势:

  • 重排参数顺序:Lambda 需要手动写参数交换,而 bind 一句搞定。
  • 绑定成员函数:Lambda 里需要写 [&stu](int x) { stu.printScore(x); },而 bind 更简洁。
  • 配合 STL 算法:将二元谓词固定一个参数变成一元谓词时,bind 写起来很快:
#include <vector>
#include <algorithm>
// 找第一个小于5的数
vector<int> nums = {1, 5, 8, 12, 3};
auto it = std::find_if(nums.begin(), nums.end(),
                       std::bind(std::greater<int>(), 5, _1));
// 等价于: 5 > x,即 x < 5

当然,Lambda 也能写:[](int x){ return x < 5; },哪个更清晰自见分晓。建议:如果 bind 一行能搞定,且不涉及复杂捕获,可以用 bind;否则用 Lambda。


三、两者结合:把绑定的结果放进万能盒子

std::function 可以存储任何可调用对象,自然也包括 std::bind 返回的结果。这样我们就可以把固定了参数的函数放到容器里,统一管理。

#include <functional>
#include <vector>
#include <iostream>
using namespace std::placeholders;

int add(int a, int b) { return a + b; }

int main() {
    // 定义一个存储“接受一个int,返回int”的盒子的容器
    std::vector<std::function<int(int)>> tasks;

    // 放入绑定了第一个参数为5的add
    tasks.push_back(std::bind(add, 5, _1));   // 等价于 add(5, ?)

    // 放入绑定了第一个参数为10的add
    tasks.push_back(std::bind(add, 10, _1));

    // 循环执行所有任务
    for (auto& task : tasks) {
        std::cout << task(3) << " ";   // 第一个输出 8,第二个输出 13
    }
    return 0;
}

四、完整可运行的 C++ 示例(带详细注释)

下面代码结合了所有核心用法,并附有中文变量名注释。

#include <iostream>
#include <functional>   // std::function, std::bind, std::ref
#include <vector>
#include <algorithm>    // std::find_if, std::for_each
#include <string>
using namespace std;
using namespace std::placeholders;  // _1, _2 等占位符

// 普通函数
int multiply(int a, int b) {
    return a * b;
}

// 类:计算器
class Calculator {
public:
    int divide(int a, int b) const {   // 成员函数
        return a / b;
    }
    static int subtract(int a, int b) { // 静态成员函数
        return a - b;
    }
};

int main() {
    // ========== 1. std::function 基础 ==========
    cout << "=== std::function 基本使用 ===" << endl;

    // 装 Lambda 的盒子
    std::function<int(int,int)> box = [](int x, int y) { return x + y; };
    cout << "box(3,4) = " << box(3,4) << endl;              // 7

    // 装普通函数
    box = multiply;
    cout << "box(6,7) = " << box(6,7) << endl;              // 42

    // 装仿函数
    struct Doubler {
        int operator()(int a, int b) { return (a + b) * 2; }
    };
    box = Doubler();
    cout << "box(10,20) = " << box(10,20) << endl;          // 60

    // 把各种盒子装到一个容器里(回调列表)
    vector<std::function<int(int,int)>> callbacks;
    callbacks.push_back([](int a, int b) { return a * b; });
    callbacks.push_back(multiply);
    callbacks.push_back([](int a, int b) { return a + b + 1; });
    for (auto& cb : callbacks) {
        cout << "回调结果: " << cb(2, 3) << " ";            // 6 6 6
    }
    cout << endl;

    // ========== 2. std::bind 基础 ==========
    cout << "\n=== std::bind 基本使用 ===" << endl;

    // 固定第一个参数为2的乘函数:multiply(2, ?)
    auto doubleIt = bind(multiply, 2, _1);
    cout << "doubleIt(5) = " << doubleIt(5) << endl;        // 10

    // 固定第二个参数为10的乘函数:multiply(?, 10)
    auto timesTen = bind(multiply, _1, 10);
    cout << "timesTen(7) = " << timesTen(7) << endl;        // 70

    // 重排参数顺序:multiply(b, a)
    auto reversed = bind(multiply, _2, _1);
    cout << "reversed(3,4) = " << reversed(3,4) << endl;    // multiply(4,3)=12

    // 绑定成员函数(需要对象指针)
    Calculator calc;
    std::function<int(int,int)> divideFunc = bind(&Calculator::divide, &calc, _1, _2);
    cout << "divideFunc(10,3) = " << divideFunc(10,3) << endl; // 3

    // 绑定静态成员函数(无需对象)
    std::function<int(int,int)> subFunc = bind(&Calculator::subtract, _1, _2);
    cout << "subFunc(10,3) = " << subFunc(10,3) << endl;     // 7

    // 引用绑定:修改外部变量
    int x = 100;
    auto addToRef = bind([](int &a, int b) { a += b; }, ref(x), _1);
    addToRef(50);
    cout << "x 被修改为: " << x << endl;                    // 150

    // ========== 3. 在STL算法中使用bind(固定二元谓词)==========
    cout << "\n=== bind 在算法中的应用 ===" << endl;
    vector<int> nums = {1, 5, 8, 12, 3};

    // 找第一个小于5的元素:greater<int>()(5, x) 等价于 5 > x
    auto it = find_if(nums.begin(), nums.end(),
                      bind(greater<int>(), 5, _1));
    if (it != nums.end()) {
        cout << "第一个小于5的元素是: " << *it << endl;      // 1
    }

    // 更简单的写法:用Lambda(推荐)
    auto it2 = find_if(nums.begin(), nums.end(),
                       [](int x) { return x < 5; });
    // 结果一样

    // ========== 4. 结合使用:回调注册系统 ==========
    cout << "\n=== 回调系统示例 ===" << endl;

    class Button {
    public:
        // 注册点击回调
        void setOnClick(std::function<void()> callback) {
            onClickCallback = callback;
        }
        // 模拟点击
        void click() {
            if (onClickCallback) {
                onClickCallback();
            }
        }
    private:
        std::function<void()> onClickCallback;
    };

    Button button;
    int clickCount = 0;   // 记录点击次数
    // 注册一个Lambda作为回调(捕获点击次数变量引用)
    button.setOnClick([&clickCount]() {
        clickCount++;
        cout << "按钮被点击,总次数: " << clickCount << endl;
    });
    // 模拟三次点击
    button.click();   // 输出1
    button.click();   // 输出2
    button.click();   // 输出3

    return 0;
}

五、Python 中的对应实现

Python 中,functools.partial 相当于 std::bind(固定部分参数,但不能重排参数顺序)。而可调用对象的统一容器正是 Python 动态类型的优势——任何可调用对象(函数、Lambda、类方法)都可以直接放到列表里,无需 std::function

from functools import partial
from typing import Callable, List

def multiply(a, b):
    return a * b

class Calculator:
    def divide(self, a, b):
        return a // b
    @staticmethod
    def subtract(a, b):
        return a - b

# 1. 类似 std::function:直接赋值
func1: Callable[[int, int], int] = lambda x, y: x + y
print("func1(3,4) =", func1(3, 4))   # 7

# 容器存储回调
callbacks: List[Callable[[int, int], int]] = []
callbacks.append(lambda a, b: a * b)
callbacks.append(multiply)
for cb in callbacks:
    print("回调结果:", cb(2, 3))      # 6 6

# 2. 类似 std::bind:partial
double_it = partial(multiply, 2)       # 固定第一个参数
print("double_it(5) =", double_it(5)) # 10

# 不能重排参数,但可以用 lambda 代替
reversed_func = lambda a, b: multiply(b, a)
print("reversed(3,4) =", reversed_func(3,4))  # 12

# 绑定成员函数
calc = Calculator()
divide_func = lambda a, b: calc.divide(a, b)
print("divide_func(10,3) =", divide_func(10,3))  # 3

# 静态成员直接引用
sub_func = Calculator.subtract
print("sub_func(10,3) =", sub_func(10,3))  # 7

# 引用绑定:Python 的闭包本身就是引用,无需特殊语法
x = [100]   # 用列表模拟可变
def add_to(n):
    x[0] += n
capture_ref = partial(add_to, 50)  # 注意:这里partial绑定了函数,但函数内部修改的是外部列表
capture_ref()
print("x 修改后:", x[0])  # 150

# 3. 算法中使用固定参数
nums = [1, 5, 8, 12, 3]
it = next((x for x in nums if x < 5), None)
print("第一个小于5的元素是:", it)   # 1

# 4. 回调系统
class Button:
    def __init__(self):
        self.callback = None
    def on_click(self, callback):
        self.callback = callback
    def click(self):
        if self.callback:
            self.callback()

button = Button()
click_count = 0
def on_click_cb():
    nonlocal click_count
    click_count += 1
    print("点击次数:", click_count)
button.on_click(on_click_cb)
button.click()   # 1
button.click()   # 2

六、常见错误与避坑指南

  1. 忘记 #include <functional>

    • 编译器报错:‘function’ is not a member of ‘std’
  2. 占位符 _1, _2 未指定命名空间

    • 需要在 std::placeholders 命名空间中,或者 using namespace std::placeholders;。否则会报错 ‘_1’ was not declared
  3. bind 绑定成员函数时忘了传对象

    • 对于非静态成员函数,必须传对象指针(&obj)或对象引用(但 std::ref(obj) 也可以)。否则编译错误。
  4. 引用绑定忘记 std::ref

    • 默认按值拷贝,内部修改不会影响外部。如果需要引用语义,务必用 std::refstd::cref
  5. std::function 性能陷阱

    • 每次调用比直接函数调用多一层间接和可能的动态内存分配。在循环中频繁调用几百万次时,可以考虑函数指针或模板。
  6. 过度使用 std::bind 导致代码晦涩

    • 如果表达式嵌套多层 bind 和占位符,可读性很差。此时改用 Lambda 更清晰。

七、总结与进一步学习

  • std::function:万能函数盒子,让不同类型的可调用对象拥有同一类型,适合容器存储、回调注册、接口抽象。
  • std::bind:参数胶水,固定部分参数、调整参数顺序,常用于适配已有函数到新签名。
  • 两者结合:std::function 存储绑定的结果,可以灵活构建任务队列或事件系统。
  • C++11 以后,Lambda 通常比 std::bind 更直观,但 std::bind 在参数重排和绑定成员函数时仍有简洁优势。

相关知识点推荐(你可以继续学习):

  • Lambda 表达式:现代 C++ 更常用的可调用对象生成方式,语法更清晰,支持局部捕获。
  • 仿函数(函数对象):重载 operator() 的类,可以保存状态。
  • std::mem_fn:专门用于生成成员函数包装器,比 bind 更简洁。
  • std::not_fn:对谓词取反,C++17 引入。
  • 策略模式与回调设计:在游戏、GUI、网络库中广泛应用。

掌握了 std::functionstd::bind,你就可以像搭积木一样灵活组合函数,写出更优雅、更易维护的 C++ 代码。

例题精讲

1单选题

要使用std::function,需要包含哪个头文件?

A<functional>
B<function>
C<algorithm>
D<utility>
2判断题

std::function可以存储lambda表达式,但不能存储普通函数指针。

3填空题
给定函数 void print(int a, int b) { std::cout << a << b; },使用std::bind将第一个参数固定为10,第二个参数由调用时传入,代码为:auto f = std::bind(print, 10, ___);
4单选题

关于std::bind的返回值类型,以下说法正确的是?

A返回一个函数指针
B返回一个std::function对象
C返回一个未指定的可调用对象类型
D返回一个lambda表达式
5判断题

使用std::bind绑定时,若传递引用参数应使用std::ref,否则默认是值传递。