线程池模拟实现的后续改进想法
- 1问题:原生普通函数不能直接通过AddTask()对函数传参
- 2:解决方法:
- 1:成员函数
- 2:lambda 捕获参数
- 1:[var] 显式值捕获(拷贝)
- 2:[&var] 显式引用捕获(别名)
- 3:混合显式捕获
- 4:隐式捕获(批量捕获所有变量)
- 5:隐式 + 单独指定
- 6:初始化捕获(C++14 通用捕获)
- 7:特殊捕获规则
- 3:std::bind 绑定参数
- 1:bind默认拷贝
- 2:bind的引用拷贝
- 3:bind 绑定右值 / 移动语义:std::move
- 4:占位符 std::placeholders::_1
- 1.占位符基础含义
- 2:关键区分:别搞混「原函数形参顺序」和「占位符顺序」
- 3:搭配类成员函数 bind 常用场景
- 4:让接口原生支持传参(可变模板重载)
- 1:拆分语法:
- 2:`std::forward<Args>(args)`:完美转发
- 3:包展开语法 `...arg = std::forward<Args>(args)`
- 4:区分:三种不同位置的 `...`
- 5:简单总结
1问题:原生普通函数不能直接通过AddTask()对函数传参
template<classFunc>voidAddTask(Func&&func){PushTask(std::forward<Func>(func));}原理说明:PushTask最终把传入的可调用对象存入queue<TTask>,TTask强制是std::function<void()>。不管是普通函数、成员函数,只要原生带参数,都必须外层包一层无参包装器(bind/lambda),把参数固化进去,才能符合队列存储要求。
内部存入队列的类型是TTask = std::function<void()>,要求无参、无返回的可调用对象。
- 普通函数
void test(int a)本身签名是void(int),和void()不匹配,直接传会编译报错; - 函数的参数无法通过
AddTask接口追加,接口只接收一个可调用对象,没有参数占位。
2:解决方法:
1:成员函数
成员函数专用重载,专门处理类成员 + 对象 + 参数:将this指针bind给普通函数,方便函数后续访问类内成员。
template<classClassType,classFunc>voidAddTask(ClassType*obj,Func&&func){PushTask(std::bind(std::forward<Func>(func),obj));}2:lambda 捕获参数
[捕获规则] (参数) -> 返回值 { 函数体 };[]决定 lambda 能访问外部变量,分为值捕获、引用捕获、隐式捕获、this 捕获、初始化捕获五大类。
voidPrintMsg(conststd::string&msg,intnum){std::cout<<msg<<" "<<num<<std::endl;}std::string s="test";intn=123;// lambda捕获变量,封装成无参任务pool.AddTask([s,n](){PrintMsg(s,n);});lambda 捕获语法详解:
| 捕获写法 | 含义 |
|---|---|
[] | 不捕获任何局部变量 |
[x] | 按值捕获变量x |
[&x] | 按引用捕获变量x |
[=] | 按值捕获所有使用到的局部变量 |
[&] | 按引用捕获所有使用到的局部变量 |
[=, &x] | 默认按值捕获,但x按引用捕获 |
[&, x] | 默认按引用捕获,但x按值捕获 |
[this] | 捕获当前对象的指针(this) |
[*this] | 拷贝整个当前对象(C++17) |
[a = expr] | 初始化捕获(C++14),用expr初始化a |
1:[var] 显式值捕获(拷贝)
- 把外部变量复制一份到 lambda,内部修改不影响外部原变量
- 捕获的变量默认
const,不可修改;要改需加mutable,不是变量本身 const,而是 lambda 默认operator() const,const 成员函数无法修改非 mutable 成员;加 mutable 移除调用运算符的 const 限定,不是修改捕获变量的 const 属性。
inta=10;autof=[a](){a=20;// 报错,const拷贝cout<<a;};f();// mutable 允许修改拷贝副本autof2=[a]()mutable{a=20;// 只改副本,外部a不变cout<<a;};f2();cout<<a;// 输出102:[&var] 显式引用捕获(别名)
- 直接绑定外部变量的地址,内部修改同步影响外部
- 生命周期风险:若 lambda 生命周期长于外部变量,会悬垂引用
inta=10;autof=[&a](){a=20;};f();cout<<a;// 输出203:混合显式捕获
同时捕获值、引用,逗号分隔:
intx=1,y=2;autof=[x,&y](){// x只读,y可修改y=x+10;};4:隐式捕获(批量捕获所有变量)
[=]全部值捕获,[&]全部引用捕获
inta=1,b=2;autof=[=](){cout<<a+b;};intc=1;autof=[&](){c=100;};5:隐式 + 单独指定
批量默认捕获,个别变量反向控制:
[=, &x]:默认全部值捕获,唯独 x 用引用[&, x]:默认全部引用捕获,唯独 x 用值拷贝- 禁止冲突写法:
[=, =x]、[&, &x],重复指定报错。
inta=1,b=2,c=3;// a、c值捕获,b引用捕获autof=[=,&b](){b=a+c;};6:初始化捕获(C++14 通用捕获)
语法:[变量名 = 表达式]
作用:
- 捕获临时变量、右值、移动对象
- 重命名外部变量
- 捕获无法拷贝只能移动的类型(
unique_ptr)
inta=10;// 1. 重命名捕获autof1=[b=a](){cout<<b;};// 2. 移动捕获(std::move)autop=std::make_unique<int>(5);autof2=[p=std::move(p)](){cout<<*p;};// 原p已空,所有权转移到lambda内部// 3. 捕获临时表达式autof3=[x=a*2](){cout<<x;};[&x = ...]引用初始化捕获,绑定表达式左值。
含义:不要求外部有变量 x,直接把右边表达式...的结果,用左值引用绑定到捕获变量x
7:特殊捕获规则
[ ]空捕获:不捕获任何外部变量,能使用 lambda 自身参数、函数内局部变量,静态局部变量。
intg=100;autof=[](){cout<<g;};3:std::bind 绑定参数
// 普通带参函数voidPrintMsg(conststd::string&msg,intnum){std::cout<<msg<<" "<<num<<std::endl;}// 使用线程池ThreadPool pool;pool.Start();// bind 把参数提前打包成无参functionpool.AddTask(std::bind(PrintMsg,"hello",666));1:bind默认拷贝
bind 会拷贝 / 移动传入的参数存入内部存储,不是单纯 “拷贝”;传入右值会移动构造副本,传入左值拷贝
voidfunc(int&x){x+=100;}intmain(){inta=10;autotask=std::bind(func,a);// bind内部拷贝了一份a,存的是副本,不是对外部a的引用task();std::cout<<a<<std::endl;// 输出10,外部a没变,副本内部变成110return0;}2:bind的引用拷贝
想要绑定引用:必须用std::ref()(左值引用)/std::cref()(const 左值引用)
std::ref/std::cref会生成一个引用包装器std::reference_wrapper<T>,bind 识别该类型后,会存储引用而不是拷贝。
voidfunc(int&x){x+=100;}intmain(){inta=10;autotask=std::bind(func,std::ref(a));task();std::cout<<a<<std::endl;// 输出110,外部变量被修改return0;}3:bind 绑定右值 / 移动语义:std::move
bind可以接收右值,但 bind 内部会永久保存为左值,想要转移资源,需要手动包std::move:
voidfunc(std::string s){}voidfunc_rref(std::string&&s){}intmain(){std::string str="hello";// 传入右值临时,bind 允许autob1=std::bind(func,std::string("temp"));// 传入 move 右值,bind 也允许autob2=std::bind(func,std::move(str));return0;}bind 把所有传入的参数以实体对象存在内部成员里;
当你后续调用b1()、b2()时,bind 拿内部存储的对象传给目标函数,这个内部对象是有名字的成员变量 →属于左值。
voidfunc_rref(std::string&&s){std::cout<<"右值引用版本\\n";}voidfunc(std::string s){std::cout<<"值拷贝版本\\n";}intmain(){std::string s="test";autob=std::bind(func,std::move(s));b();// 正常运行,但内部左值拷贝,不移动autob=std::bind(func_rref,std::move(s));b();// 编译报错!return0;}- 把右值存进 bind 内部:可以移动构造,没问题;
- 调用 bind、把内部参数传给目标函数:bind 无法自动移动 / 转右值,永远传左值;
- 不可拷贝类型(unique_ptr)无法使用 bind,直接编译失败,unique_ptr 只允许移动、禁止拷贝;
4:占位符 std::placeholders::_1
bind 预留参数位,后续调用传参时也可以配合 ref:
voidfunc(inta,int&b){b=a;}intmain(){intx=0;autotask=std::bind(func,std::placeholders::_1,std::ref(x));task(666);std::cout<<x;// 666}1.占位符基础含义
_1、_2、_3… 都是占位符:
_1:调用bind生成的可调用对象时,第一个传入的参数_2:调用时第二个传入参数_n:第 n 个传入参数
voidfunc(inta,intb){cout<<a<<" "<<b;}intmain(){// 把 func 的第一个参数固定为 10,第二个参数用调用时的第1个实参(_1)填充autof=bind(func,10,placeholders::_1);f(20);// 调用时传入20,对应 _1// 输出:10 20}2:关键区分:别搞混「原函数形参顺序」和「占位符顺序」
占位符只和bind 生成的新函数调用时的参数挂钩,和原函数参数无关。
voidfunc(intx,inty){cout<<x<<","<<y;}intmain(){// _2 是调用 f 时的第2个参数,_1 是第1个autof=bind(func,placeholders::_2,placeholders::_1);f(100,200);// 调用 f 时:_1=100,_2=200// 传给 func:x=_2=200,y=_1=100// 输出:200,100}3:搭配类成员函数 bind 常用场景
成员函数第一个隐参是this,占位符依然指代调用包装对象时的参数:
structTest{voidtest(intv){cout<<v;}};intmain(){Test obj;autof=bind(&Test::test,&obj,placeholders::_1);f(666);// _1 = 666,输出666}4:让接口原生支持传参(可变模板重载)
voidPrintMsg(conststd::string&msg,intnum){std::cout<<msg<<" "<<num<<std::endl;}template<classFunc,class...Args>voidAddTask(Func&&func,Args&&...args){// 自动绑定参数生成无参任务autotask=[f=std::forward<Func>(func),...arg=std::forward<Args>(args)](){f(arg...);};PushTask(std::move(task));}pool.AddTask(PrintMsg,"abc",789);1:拆分语法:
template<classFunc,class...Args>voidAddTask(Func&&func,Args&&...args)Args是参数包名;...代表可变,可以接收 0/N 个任意类型模板参数;- 调用时编译器自动推导所有实参类型打包进
Args。
Args&&:万能引用(转发引用)
普通T&&是右值引用;
但模板参数 + &&组合 =万能引用,能同时绑定左值、右值:
- 传入左值变量:
Args推导为T&→Args&&=T& &&折叠成T&(左值引用) - 传入临时右值:
Args推导为T→Args&&=T&&(右值引用)
Args&&... args:参数包 + 函数形参包
args是存储所有实参的函数形参包;...写在类型后面:表示把Args里每一种类型都生成一个Args&&形参;
等价于展开:
voidprint(int,std::string,double);AddTask(print,10,"hello",3.14);// 三个参数时展开voidAddTask(Func&&func,int&&a,string&&b,double&&c)2:std::forward<Args>(args):完美转发
作用
保留实参原本的左 / 右值属性,不丢失移动语义:
- 左值传入 → 转发后仍是左值
- 临时右值传入 → 转发后仍是右值,支持
std::move转移资源
原理std::forward是带条件转换的 static_cast:
template<classT>T&&forward(std::remove_reference_t<T>&t)noexcept{returnstatic_cast<T&&>(t);}3:包展开语法...arg = std::forward<Args>(args)
autotask=[f=std::forward<Func>(func),...arg=std::forward<Args>(args)](){f(arg...);};- 捕获展开
...arg=xxx...arg=std::forward<Args>(args)会对参数包里每一个元素生成捕获项:
调用AddTask(print,10,"hi",3.14)等价展开:
[f=forward(print),arg0=forward(10),arg1=forward("hi"),arg2=forward(3.14)]把所有函数参数转发捕获到 lambda 内部,保存一份副本。
- 调用展开
f(arg...)arg...是调用时参数包展开,把捕获的所有变量依次传给函数:
f(arg0,arg1,arg2);4:区分:三种不同位置的...
- 模板参数
class... Args:声明可变参数包 - 形参
Args&&... args:声明可变参数包 - 捕获 / 调用
...arg/args...:使用时展开包
| 写法位置 | 位置作用 | 作用 |
|---|---|---|
...arg = xxx | lambda 捕获[]包名前面 | 批量创建一组捕获变量(定义包) |
arg... | 函数调用()包名后面 | 把已有包拆成多个参数(使用包) |
class... Args | 模板参数声明 | 声明可变参数包 |
Args&&... args | 函数形参声明 | 声明可变参数包 |
voidfunc(std::unique_ptr<int>&&ptr){}template<classFunc,class...Args>voidAddTask(Func&&func,Args&&...args){PushTask(std::bind(std::forward<Func>(func),std::forward<Args>(args)...));}//编译失败:bind内部是左值,无法绑定T&&右值引用template<classFunc,class...Args>voidAddTask(Func&&func,Args&&...args){// 自动绑定参数生成无参任务autotask=[f=std::forward<Func>(func),...arg=std::forward<Args>(args)](){f(arg...);};PushTask(std::move(task));}autotask=[f=std::forward<Func>(func),...arg=std::forward<Args>(args)](){f(arg...);};5:简单总结
class... Args:定义一组任意数量、任意类型的模板参数包;Args&&... args:万能引用形参包,同时接收左值 / 右值;std::forward<Args>(args):完美转发,保留原始左 / 右值属性;...arg=xxx/args...:循环展开参数包里所有元素;