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 引用
浙公网安备 33010602011771号