1. 可调用对象

  • 可以使用函数调用运算符()执行的对象,也就是说,只要能写成obj(args...),这个obj就是可调用对象。
  • C++把 “普通函数、函数指针、lambda、仿函数、成员函数” 统一抽象成一类东西 ——可调用对象,方便模板统一接收。

2. C++5大类可调用对象

2.1 普通函数

#include <iostream>
void foo(int a)
{
    std::cout << a << "\n";
}

int main()
{
    foo(10);   // foo 是可调用对象
    return 0;
}

2.2 函数指针

#include <iostream>

void foo(int a)
{
    std::cout << a << "\n";
}
int main()
{
    //fp是函数指针,也是可调用对象
    void(*fp)(int) = foo;
    fp(20);
    return 0;
}

2.3 仿函数:重载operator()

  • 对象可以像函数一样obj()调用
#include <iostream>
struct Functor
{
    int base;
    Functor(int b):base(b){}
    void operator()(int x)
    {
        std::cout << x + base << "\n";
    }
};
int main()
{
    Functor f(100);
    f(5);   //等价 f.operator()(5),可调用对象
    return 0;
}

2.4 Lambda 表达式

  • 编译器会自动生成一个匿名仿函数类。[]捕获就是仿函数的成员变量。
#include <iostream>
int main()
{
    int val = 10;
    auto lam = [val](int x){
        std::cout << val + x << "\n";
    };
    lam(3);
    return 0;
}

2.5 成员函数(静态 / 非静态)

  • 非静态成员函数不能直接调用,必须绑定对象。
#include <iostream>
struct Demo
{
    void func(int x){ std::cout << x << "\n"; }
    static void s_func(int x){ std::cout << x << "\n"; }
};
int main()
{
    Demo d;
    //非静态成员函数,需要对象
    (d.*(&Demo::func))(100);
    Demo::s_func(200); //静态成员函数不需要对象
    return 0;
}
  • 直接写 Demo::func 不能直接传给std::function,要用std::bind或者 lambda 绑定对象。

3. 可调用对象包装器(std::function)

3.1 为什么需要对象包装器

  • 虽然上面列出的对象都可以被调用,但它们的类型各不相同:
// 三个不同类型的可调用对象
int add(int a, int b) { return a + b; }
struct Mul { int operator()(int a, int b) { return a * b; } };
auto sub = [](int a, int b) { return a - b; };
// 我们想做一个函数,接收“任何”可调用对象
// 难道要写三个重载吗?太不灵活了!
  • 于是有了std::function,它定义在 头文件中,是一个类型擦除的包装器。它可以存储、复制和调用任何符合特定签名的可调用对象。

基础语法:
std::function< R(Arg1, Arg2, ...) > 变量名;

  • R:返回值类型
  • Arg1,Arg2...:形参列表类型
    给std::function装入可调用对象:
// 1.普通全局函数
void func(int x){}
std::function<void(int)> f = func;

// 2.lambda
f = [](int x){};

//3.仿函数对象
struct F{ void operator()(int){} };
f = F{};

//4.std::bind返回的可调用对象
auto b = std::bind(func, std::placeholders::_1);
f = b;
#include <functional>
#include <iostream>
// 统一包装为:接受两个int,返回int
std::function<int(int, int)> func;
func = add;          // 赋值普通函数
std::cout << func(1, 2) << std::endl; // 3
func = Mul();        // 赋值仿函数
std::cout << func(3, 4) << std::endl; // 12
func = [](int a, int b) { return a - b; }; // 赋值Lambda
std::cout << func(10, 3) << std::endl; // 7

3.2 适用场景

  • 线程池任务队列
    把各种各样带参函数,通过std::bind打包成无参std::function<void()>存入队列。
std::queue<std::function<void()>> tasks;
// enqueue内部:bind把函数+参数打包,得到无参可调用对象,存入function
// 工作线程取出任务直接 task();
  • 回调函数
    类保存回调成员,外部注册 lambda / 函数,事件触发执行回调
#include <functional>
#include <iostream>

class Button
{
public:
    //回调成员变量
    std::function<void()> click_callback;

    void click()
    {
        if(click_callback)
            click_callback();
    }
};

int main()
{
    Button btn;
    //注册lambda回调
    btn.click_callback = [](){ std::cout << "按钮被点击\n"; };
    btn.click();
    return 0;
}
  • 作为函数参数,接收回调
//接收一个回调
void do_work(std::function<void(int)> callback)
{
    callback(666);
}

int main()
{
    //传入lambda
    do_work([](int v){ std::cout << v << "\n"; });
    return 0;
}
  • 策略模式,运行时切换行为
std::function<int(int,int)> op;

op = [](int a,int b){return a+b;};
std::cout << op(1,2);

op = [](int a,int b){return a-b;};
std::cout << op(1,2);

4. std::function 注意事项

4.1 空对象调用抛异常 std::bad_function_call

std::function<void()> f;
f(); //崩溃,抛出异常

4.2 lambda 捕获生命周期问题

std::function会拷贝 lambda;如果 lambda 捕获引用,要保证引用对象生命周期长于 function。

std::function<void()> f;
{
    int x = 10;
    // 捕获局部变量引用,出作用域x销毁,悬挂引用!UB
    f = [&x](){ std::cout << x; };
}
f(); // 未定义行为!

4.3 非静态成员函数不能直接赋值给 std::function

成员函数类型是 void(Class:😗)(int),必须绑定类实例对象,用std::bind或者 lambda 包裹。

struct A{
    void hello(int v){}
};
A a;
// 错误 std::function<void(int)> f = &A::hello;

// 方案1 bind绑定对象
std::function<void(int)> f1 = std::bind(&A::hello, &a, std::placeholders::_1);

// 方案2 lambda包裹(可读性好)
std::function<void(int)> f2 = [&a](int v){ a.hello(v); };

4.4 std::function 可以移动,减少拷贝

std::function<void()> task = std::bind(...);
tasks.emplace(std::move(task)); //移动,避免拷贝

5. std::bind

  • 绑定可调用对象与其参数,生成一个新的可调用对象(仿函数)。
  • 可以固定部分参数、重排参数顺序、占位符接收后续传入参数。
  • 返回的结果对象,可以直接赋值给 std::function。

5.1 为什么需要std::bind

  • 预先固定一部分参数,剩下参数以后再传入;
  • 调整参数顺序;
  • 把多参数函数包装成无参函数;
  • 绑定类成员函数与类对象(成员函数必须依赖对象才能调用)

5.2 如果没有 bind,能怎么做?

  • 只能手写 lambda 包装,也能实现,但是 C++11 之前没有 lambda,只能手写仿函数。
  • std::bind相当于编译器帮你自动生成仿函数。

5.3 语法格式

auto new_callable = std::bind( func, arg1, arg2, ... );

  • func:原始可调用对象(函数、函数指针、仿函数、成员函数指针)
  • arg1,arg2...:绑定的参数,可以是实际值,也可以是占位符 _1,_2,_3
  • 返回值:未指定类型的 bind 仿函数对象,一般用auto接收,或者存入std::function
    占位符命名空间:std::placeholders::_1,代表调用新对象时传入的第 1 个参数
占位符 含义
std::placeholders::_1 调用 bind 返回对象时传入的第 1 个实参
std::placeholders::_2 调用 bind 返回对象时传入的第 2 个实参
void foo(int a,int b){
    std::cout << a << " " << b <<std::endl;
}
//绑定第一个参数固定为10,第二个参数留到调用时传入
auto f = std::bind(foo, 10, std::placeholders::_1);
f(20); //等价 foo(10, 20)

5.3.1 固定全部参数 → 生成无参可调用对象

void add(int a,int b){ std::cout << a+b << std::endl; }

auto task = std::bind(add, 3,5);
task(); //add(3,5)
// 可以放进std::function<void()>
std::function<void()> f = std::bind(add,3,5);
f();

5.3.2固定部分参数,剩余参数用占位符延后传入

void foo(int x,int y,int z){
    std::cout<<x<<","<<y<<","<<z<<std::endl;
}
//x固定为10;后面两个参数调用时传入
auto f = std::bind(foo,10,std::placeholders::_1,std::placeholders::_2);
f(20,30); // foo(10,20,30)

5.3.3 调整参数顺序

void swap_print(int a,int b){
    std::cout << a << "|" << b << std::endl;
}
//把调用的第一个参数传给b,第二个传给a
auto f = std::bind(swap_print, std::placeholders::_2, std::placeholders::_1);
f(100,200); //等价 swap_print(200, 100)

5.3.4 绑定类成员函数

#include <functional>
#include <iostream>
struct Demo{
    void hello(int v){
        std::cout<<"hello:" << v <<std::endl;
    }
};

int main()
{
    Demo obj;
    // &Demo::hello 获取成员函数指针;第二个参数绑定对象地址
    auto call = std::bind(&Demo::hello, &obj, std::placeholders::_1);
    call(999); //obj.hello(999)

    //存入std::function
    std::function<void(int)> f = std::bind(&Demo::hello,&obj,std::placeholders::_1);
    f(123);
    return 0;
}
  • 静态成员函数不需要绑定对象,当做普通函数 bind 即可。
    参数传递语义:值拷贝 / 引用
  • std::bind默认对传入参数做值拷贝。
  • 如果想要绑定引用,需要用std::ref() / std::cref()。
void print(int& x){
    x++;
}
int main(){
    int a = 10;
    // 默认拷贝,bind内部保存一份a副本,外部a不会变
    auto f1 = std::bind(print,a);
    f1();
    // std::ref,绑定引用,操作原始a
    auto f2 = std::bind(print,std::ref(a));
    f2();
    std::cout << a << std::endl;
    return 0;
}
  • std::ref(x):绑定非常量引用
  • std::cref(x):绑定 const 引用