《C++进阶之C++11》【lambda表达式 + 包装器】
【lambda表达式 + 包装器】目录
- 前言:
- ------------ lambda表达式 ------------
- 1. 什么是lambda表达式?
- 2. lambda表达式的基本语法形式?
- 2.1:捕获列表
- 2.2:参数列表与返回值
- 3. lambda表达式怎么使用?
- 4. 特殊的捕获限制有哪些?
- 5. lambda表达式的价值是什么?
- 6. lambda表达式的应用有哪些?
- 7. lambda表达式的原理是什么?
- ------------ 包装器 ------------
- 1. 什么是包装器?
- 2. 常见的包装器有哪些?
- 2.1:函数包装器(std::function)
- 2.2:绑定器(std::bind)
- 3. 包装器怎么应用?
- 3.1:std::function的应用
- 3.2:std::bind的应用

往期《C++初阶》回顾:
《C++初阶》目录导航
往期《C++进阶》回顾:
/------------ 继承多态 ------------/
【普通类/模板类的继承 + 父类&子类的转换 + 继承的作用域 + 子类的默认成员函数】
【final + 继承与友元 + 继承与静态成员 + 继承模型 + 继承和组合】
【多态:概念 + 实现 + 拓展 + 原理】
/------------ STL ------------/
【二叉搜索树】
【AVL树】
【红黑树】
【set/map 使用介绍】
【set/map 模拟实现】
【哈希表】
【unordered_set/unordered_map 使用介绍】
【unordered_set/unordered_map 模拟实现】
/------------ C++11 ------------/
【列表初始化 + 右值引用】
【移动语义 + 完美转发】
【可变参数模板 + emplace接口 + 新的类功能】
前言:
hi ~,小伙伴们大家好啊,今天是……嗯~o ( ̄▽ ̄) o 对,今天是国庆节!是不是没料到距离上次更新才过去两天,鼠鼠就又来啦?
哈哈,偷偷告诉你们一个小秘密:每月的一号鼠鼠都会准时更新哦~~(ノ>ω<)ノ你要是不信,那就罚你等到 11 月 1 号再来验证,到时候可别忘啦来!(≧▽≦)♪
今天我们要学习的新特性是:【lambda表达式 + 包装器】。╰(▔∀▔)╯
【lambda 表达式 + 包装器】就像给 “临时工具” 配 “专属收纳盒”:→ (っ•̀ᴗ•́)っ✧
- lambda 是 C++ 里随写随用的 “临时函数”,不用单独定义函数名,写在遍历排序、过滤等场景里很轻便,但类型隐蔽,没法直接传参或复用 ╮(╯▽╰)╭
- 包装器(比如 function)就是 “收纳盒”,能把 lambda “装起来” 赋予明确类型,不管传参还是存起来反复用都能搞定,就像临时扳手放进工具盒,好拿好存不麻烦╰(✧∇✧)╯
简单说,lambda 负责 “快速造工具”,包装器负责 “好好存工具”,两者搭在一起,既能享受 lambda 的便捷,又能解决它的使用局限,写代码时灵活度和实用性直接拉满~ദ്ദി˶>ω<)✧
------------ lambda表达式 ------------
1. 什么是lambda表达式?
lambda表达式
:是一种匿名函数,也就是没有名字的函数。
它是一种简洁的函数定义方式,不需要显式命名函数
它可以方便地定义
简短的可调用对象
,用于各种需要函数对象
的场景可定义在
函数内部
(普通函数只能定义在全局
、命名空间
或类
中)语法层面无显式类型,需通过
auto
或模板参数
接收其匿名类型
主要特点:
- 匿名性:没有函数名
- 简洁性:通常用于简单操作,可以在一行内完成
- 临时性:常用于一次性使用或作为参数传递给高阶函数
2. lambda表达式的基本语法形式?
基本语法形式:
[捕获列表](参数列表) -> 返回类型 { 函数体 }[capture-list] (parameters) -> return-type { function-body }
- 捕获列表(
capture-list
):用于指定在 lambda 表达式中可以访问的外部变量,是 lambda 能够访问所在作用域中变量的关键。
- 必须出现在 lambda 最开头,编译器通过
[]
识别 lambda 开始- 它可以为空,表示不捕获任何外部变量,但是
[]
不能省略(空捕获列表是语法要求)- 参数列表(
parameters
):和普通函数的参数列表类似,用于指定 lambda 函数的参数。
- 它可以为空
- 若无需参数传递,可连同
()
一起省略(如:无参 lambda 可写[] { ... }
)- 返回值类型(
return-type
):指定 lambda 函数的返回值类型。
- 无返回值时(如:
void
),可直接省略- 返回值类型明确时(如:单一
return
语句),编译器可自动推导,也可省略- 函数体(
function-body
):定义 lambda 函数的具体操作和逻辑,也就是函数执行时要做的事情。
- 除参数外,可直接使用捕获列表中的变量
- 即使函数体为空(如:
[] {}
),{}
也不能省略
通过上面的关于lambda的语法的介绍,现在我们可以明白
空lambda
的形式了:// 空lambda(所有可省略部分均省略) auto emptyLambda = [] {}; // 合法(但无实际逻辑)
2.1:捕获列表
lambda表达式默认仅能访问自身函数体、参数列表里的变量。若需使用外层作用域(如:包含 lambda 的函数作用域)的变量,需通过 捕获列表 显式声明。
捕获列表决定了 lambda 表达式对外部变量的访问方式,常见的捕获方式有以下几种:
显示捕获
:在捕获列表中明确写出变量名,用逗号分隔多个变量,同时通过符号区分捕获方式:
值捕获:直接写变量名(如:
[x, y]
)引用捕获:变量名前加
&
(如:[&z]
)
隐式捕获
:无需逐个写变量名,用符号批量指定捕获规则,编译器会自动识别 lambda 内用到的外层变量并捕获:
- 按值隐式捕获:捕获列表写
[=]
,lambda 内用到的外层变量全部传值捕获(创建副本)- 按引用隐式捕获:捕获列表写
[&]
,lambda 内用到的外层变量全部传引用捕获(使用引用)
混合捕获
:同时用隐式捕获符号(=
或&
)和显式变量。
值捕获
:在 lambda 内部对捕获变量的修改不会影响外部的原始变量。
- 例如:
[a]
表示按值捕获变量a
#include <iostream>
using namespace std;int main()
{//1.定义一个局部变量,初始值为10int num = 10;//2.定义一个lambda表达式,通过值捕获方式获取外部变量numauto lambda = [num]()mutable{//2.1:尝试修改捕获的副本(注意:修改的是副本,不是原始变量)num = 20; //注意:此操作不会影响外部的num变量//2.2:输出捕获副本的值(此时副本已被修改为20)cout << num << endl; // 输出: 20};/* 注意事项:默认情况下,按值捕获的变量在lambda表达式内部是不可修改的(const)* * 1. 当你使用值捕获 [num] 时,lambda 内部得到的是 num 的一个副本,* 2. 但这个副本默认是 const 的,不能修改。* 3. 因此当你尝试在 lambda 内部修改 num 时,编译器会报错。* 4. 要使按值捕获的变量可以在 lambda 内部修改,需要使用 mutable 关键字*///3.调用lambda表达式,执行内部逻辑lambda();//4.输出外部num的值(未被lambda内部修改)cout << num << endl; // 输出: 10return 0;
}
引用捕获:在 lambda 内部对捕获变量的修改会直接影响外部的原始变量。
- 例如:
[&a]
,表示按引用捕获变量a
#include <iostream>int main()
{//1.定义一个局部变量 numint num = 10;//2.定义一个lambda表达式,使用引用捕获的方式捕获外部变量 numauto lambda = [&num](){//2.1:由于是引用捕获,这里对 num 的修改会直接反映到外部的 num 变量上num = 20;//2.2:输出修改后的 num 的值,此时会输出 20std::cout << num << std::endl;};/* 注意事项:* 1. [&num] 表示以引用的方式捕获变量 num,* 2. 这意味着 lambda 表达式内部对 num 的操作会直接作用于外部定义的这个 num 变量* 3. 因为它们共享同一块内存空间*///3.调用定义好的lambda表达式,执行其内部的代码逻辑lambda();//4.再次输出 num 的值std::cout << num << std::endl; //因为 lambda 表达式中通过引用捕获修改了 num,所以这里输出 20return 0;
}
隐式捕获:可以让编译器自动推导捕获列表。 分为:
- 按值隐式捕获:
[=]
- 按引用隐式捕获:
[&]
#include <iostream>
int main()
{//1.定义两个局部变量int num1 = 10;int num2 = 20;//2.lambda1:按值隐式捕获所有用到的外部变量auto lambda1 = [=](){//2.1:输出捕获的副本值std::cout << num1 << " " << num2 << std::endl; // 输出: 10 20};/* 注意事项:* [=]:表示以值捕获方式捕获所有外部变量* lambda内部使用的是外部变量的副本,无法修改原始变量*///3.lambda2:按引用隐式捕获所有用到的外部变量auto lambda2 = [&](){//3.1:直接修改外部的num1变量num1 = 30; //3.2:输出修改后的外部变量值std::cout << num1 << " " << num2 << std::endl; // 输出: 30 20};/* 注意事项:* [&]:表示以引用捕获方式捕获所有外部变量* lambda内部使用的是外部变量的引用,可以直接修改原始变量*///4.调用lambda1:输出捕获时的值副本lambda1(); // 输出: 10 20//5.调用lambda2:修改外部变量并输出lambda2(); // 输出: 30 20//6.验证外部变量确实被修改std::cout << "外部num1: " << num1 << std::endl; // 输出: 30return 0;
}
混合捕获:可以同时使用值捕获和引用捕获。
- 若开头用
[=]
(隐式传值),后续显式变量必须传引用
- 如:
[=, &x]
(其他变量隐式传值,x
显式传引用)- 若开头用
[&]
(隐式传引用),后续显式变量必须传值
- 如:
[&, x]
(其他变量隐式传引用,x
显式传值)所以:
[=, &a]
表示按值捕获除a
以外的所有用到的外部变量,而a
按引用捕获。
总结:捕获列表的常见形式
捕获列表写法 | 含义 | 示例 (外部变量 int a=10; double b=3.14; ) |
---|---|---|
[] | 不捕获任何外部变量 | lambda 内无法使用 a 、b |
[a] | 传值捕获 a (修改 lambda 内的 a 不影响外部) | lambda 内 a 是 10 的副本 |
[&b] | 传引用捕获 b (修改 lambda 内的 b 会影响外部) | lambda 内 b 是外部 b 的引用 |
[=] | 传值捕获所有用到的外部变量 | lambda 内 a 是副本,b 是副本(修改不影响外部) |
[&] | 传引用捕获所有用到的外部变量 | lambda 内 a 、b 都是引用(修改会影响外部) |
[a, &b] | 混合捕获(a 传值,b 传引用) | lambda 内 a 是副本,b 是引用 |
2.2:参数列表与返回值
参数列表:和普通函数一样,可以有不同类型的参数,也可以没有参数。
[]() { std::cout << "Hello Lambda!" << std::endl; }
没有参数,只是输出一条信息[](int a, int b) { return a + b; }
接收两个int
类型的参数并返回它们的和
返回值:
如果函数体只有一条返回语句,返回值类型可以省略,编译器会自动推导。
[](int a, int b) //编译器会自动推导返回值类型为:int { return a + b; }
如果函数体有多条语句且需要明确返回值类型,就必须显式指定。
[](int a, int b) -> bool //注意:必须显式指定返回类型:bool { if (a > b) return true; else return false; }
3. lambda表达式怎么使用?
代码案例1:lambda表达式的使用
#include <iostream>
using namespace std;int main()
{/*------------------- 完整语法示例(显式声明所有部分)-------------------*/auto add1 = [](int a, int b) -> int{return a + b;};/* 定义一个完整语法的lambda表达式:* 1. [] 空捕获列表(不捕获任何外部变量)* 2. (int a, int b) 参数列表,接收两个int类型参数* 3. -> int 显式指定返回值类型为int* 4. { return a + b; } 函数体,实现两数相加*/cout << "add1(3,4) = " << add1(3, 4) << endl;/*------------------- 省略返回值类型(编译器自动推导)-------------------*/auto add2 = [](int a, int b){return a + b; // 返回值类型被自动推导为int};/* 省略返回值类型,编译器根据return语句自动推导返回类型为int* 其他部分与add1完全相同*/cout << "add2(5,6) = " << add2(5, 6) << endl;/*------------------- 省略参数列表(无参场景)-------------------*/ auto greet = []{std::cout << "Hello Lambda" << endl;};/* 省略参数列表的写法:当lambda不需要参数时,()可以连同参数一起省略* 但[]和{}不能省略*/greet();/*------------------- 省略捕获列表和参数列表(最简形式)-------------------*/auto emptyLambda = [] {};/* 最简lambda形式:空捕获列表+空参数列表* 可以理解为一个无输入、无输出的函数*/emptyLambda(); //调用空lambda(无任何效果,但语法合法)return 0;
}
代码案例2:lambda表达式的使用
#include <iostream>
using namespace std;//定义全局变量 x
int x = 0;int main()
{// ====================== 1. “全局变量”与“空捕获列表” ======================auto func1 = []() //注意:全局变量无需捕获即可使用,lambda 捕获列表必须为空{x++; };// ====================== 2. 显式捕获(传值 + 传引用) ======================//1.定义局部变量 a,b,c,dint a = 0, b = 1, c = 2, d = 3;//2.显式捕获:a 传值(const 只读),b 传引用(可修改)auto func2 = [a, &b](){// a++; // 错误:传值捕获的变量默认 const,无法修改(除非加 mutable)b++; // 正确:传引用捕获,可直接修改外部变量 bint ret = a + b; // a=0(原值), b=2(已自增)return ret; };cout << func2() << endl; // ====================== 3. 隐式传值捕获([=]) ======================//3.隐式传值捕获所有用到的变量(a, b, c)auto func3 = [=](){// a++; // 错误:传值捕获默认 const,无法修改int ret = a + b + c; // a=0, b=2, c=2return ret; };cout << func3() << endl; // ====================== 4. 隐式传引用捕获([&]) ======================//4.隐式传引用捕获所有用到的变量(a, c, d)auto func4 = [&](){a++; // 正确:传引用,修改外部 a → a=1c++; // 正确:传引用,修改外部 c → c=3d++; // 正确:传引用,修改外部 d → d=4};func4();cout << a << " " << b << " " << c << " " << d << endl;// ====================== 5. 混合捕获(隐式传引用 + 显式传值) ======================//5. 隐式传引用(&) + 显式传值(a, b)//规则:隐式符号(&)在前,显式变量必须传值auto func5 = [&, a, b](){// a++; // 错误:a 是显式传值,默认 const// b++; // 错误:b 是显式传值,默认 constc++; // 正确:隐式传引用,修改外部 c → c=4d++; // 正确:隐式传引用,修改外部 d → d=5return a + b + c + d; // 0 + 1 + 4 + 5 = 10};func5();cout << a << " " << b << " " << c << " " << d << endl;// ====================== 6. 混合捕获(隐式传值 + 显式传引用) ======================//6.隐式传值(=) + 显式传引用(&a, &b)//规则:隐式符号(=)在前,显式变量必须传引用auto func6 = [=, &a, &b](){a++; // 正确:显式传引用,修改外部 a → a=2b++; // 正确:显式传引用,修改外部 b → b=3// c++; // 错误:c 是隐式传值,默认 const// d++; // 错误:d 是隐式传值,默认 constreturn a + b + c + d; // 2 + 3 + 4 + 5 = 14};func6();cout << a << " " << b << " " << c << " " << d << endl;// ====================== 7. “静态变量”与“全局变量”的访问 ======================//静态局部变量:无需捕获即可使用static int m = 0;auto func7 = [](){int ret = x + m; // 全局变量 x=0,静态变量 m=0return ret; // 返回 0 + 0 = 0};cout << func7() << endl;// ====================== 8. mutable 修饰符(突破传值捕获的 const 限制) ======================//隐式传值捕获([=]) + mutable:允许修改传值副本(不影响外部变量)auto func8 = [=]() mutable{a++; // 修改传值副本 a → 副本 a=3(外部 a 仍为 2)b++; // 修改传值副本 b → 副本 b=4(外部 b 仍为 3)c++; // 修改传值副本 c → 副本 c=5(外部 c 仍为 4)d++; // 修改传值副本 d → 副本 d=6(外部 d 仍为 5)return a + b + c + d; // 3 + 4 + 5 + 6 = 18};//输出内部变量的和cout << func8() << endl; // 输出: 18//输出外部变量cout << a << " " << b << " " << c << " " << d << endl; //a=2, b=3, c=4, d=5(未被修改)return 0;
}
4. 特殊的捕获限制有哪些?
lambda表达式定义的位置:
若lambda 若定义在函数局部作用域(如:main 函数内 ):
- 仅能捕获 lambda 定义位置之前 的局部变量
- 静态局部变量(static int var; )和 全局变量 无需捕获,可直接在 lambda 内使用(因为:它们的作用域不受函数栈限制 )
若 lambda 定义在全局作用域(函数外 ):
- 捕获列表必须为空(无外层局部变量可捕获 )
mutable修饰符:突破传值捕获的只读限制:
默认情况下,传值捕获的变量在 lambda 内是
const
只读的若需修改传值捕获的副本,可在参数列表后加
mutable
,此时参数列表不能省略(即使无参数,也要写 () )
5. lambda表达式的价值是什么?
lambda表达式通过
简洁语法
实现了匿名函数对象
,核心价值在于:
- 支持函数内部定义,灵活捕获局部变量
- 语法各部分可按需省略,适配不同场景(如:无参、无返回值等)
- 配合 auto 使用,无需显式定义函数类型,让代码更简洁
掌握 lambda 的语法结构和捕获规则,是高效使用 C++ 现代特性(如:算法库、异步编程)的基础。
6. lambda表达式的应用有哪些?
在学习 lambda 表达式之前,我们所使用的
可调用对象
只有函数指针
和仿函数对象
- 函数指针的类型定义较为繁琐
- 仿函数需要定义一个类,操作相对麻烦
而使用 lambda 来定义可调用对象,简单又便捷。
lambda 在很多场景中也十分好用:
- 在线程里定义线程的执行函数逻辑
- 在智能指针中定制删除器等
lambda 的应用场景广泛,后续我们会不断接触到相关用法。
代码案例:lambda表达式应用的优越性(与传统的仿函数进行对比)
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
using namespace std;/*----------------------“商品结构体”定义----------------------*/
struct Goods
{/*------------成员变量------------*/string _name; // 商品名称double _price; // 商品价格int _evaluate; // 商品评价/*------------成员函数------------*/// 1.构造函数:初始化商品信息Goods(const char* str, double price, int evaluate): _name(str), _price(price), _evaluate(evaluate){}
};/*----------------------“打印商品信息的函数对象”----------------------*/
struct PrintGoods
{void operator()(const Goods& goods) const{cout << "名称:" << goods._name<< ",价格:" << goods._price<< ",评价:" << goods._evaluate << endl;}
};/*----------------------仿函数:价格升序比较(less)----------------------*/
struct ComparePriceLess
{bool operator()(const Goods& gl, const Goods& gr) const{return gl._price < gr._price; // 价格低的排前面}
};/*----------------------仿函数:价格降序比较(greater)----------------------*/
struct ComparePriceGreater
{bool operator()(const Goods& gl, const Goods& gr) const{return gl._price > gr._price; // 价格高的排前面}
};int main()
{// 1.初始化商品列表vector<Goods> v ={{"苹果", 2.1, 5}, // 商品1{"香蕉", 3, 4}, // 商品2{"橙子", 2.2, 3}, // 商品3{"菠萝", 1.5, 4} // 商品4};//2.先输出初始商品列表cout << "初始商品列表:" << endl;for_each(v.begin(), v.end(), PrintGoods());cout << "------------------------" << endl;// -------------------- “仿函数”排序 --------------------// 1. 按价格升序排序sort(v.begin(), v.end(), ComparePriceLess());cout << "按价格升序排序(仿函数)结果:" << endl;for_each(v.begin(), v.end(), PrintGoods());cout << "------------------------" << endl;// 2. 按价格降序排序sort(v.begin(), v.end(), ComparePriceGreater());cout << "按价格降序排序(仿函数)结果:" << endl;for_each(v.begin(), v.end(), PrintGoods());cout << "------------------------" << endl;// -------------------- “lambda表达式”排序 --------------------// 优势:无需定义额外类,直接在排序处写比较逻辑// 1. 按价格升序排序sort(v.begin(), v.end(),[](const Goods& g1, const Goods& g2) // lambda 替代 ComparePriceLess{return g1._price < g2._price;});cout << "按价格升序排序(lambda)结果:" << endl;for_each(v.begin(), v.end(), PrintGoods());cout << "------------------------" << endl;// 2. 按价格降序排序sort(v.begin(), v.end(),[](const Goods& g1, const Goods& g2) // lambda 替代 ComparePriceGreater{return g1._price > g2._price;});cout << "按价格降序排序(lambda)结果:" << endl;for_each(v.begin(), v.end(), PrintGoods());cout << "------------------------" << endl;// 3. 按评价升序排序(新需求,仿函数需额外定义类,lambda 直接写)sort(v.begin(), v.end(),[](const Goods& g1, const Goods& g2){return g1._evaluate < g2._evaluate;});cout << "按评价升序排序(lambda)结果:" << endl;for_each(v.begin(), v.end(), PrintGoods());cout << "------------------------" << endl;// 4. 按评价降序排序(新需求,lambda 直接写)sort(v.begin(), v.end(),[](const Goods& g1, const Goods& g2){return g1._evaluate > g2._evaluate;});cout << "按评价降序排序(lambda)结果:" << endl;for_each(v.begin(), v.end(), PrintGoods());cout << "------------------------" << endl;return 0;
}
7. lambda表达式的原理是什么?
lambda
的原理和范围for
类似,从编译后的汇编指令层面来看,不存在 lambda 和范围for 这些语法结构。
范围for
底层基于迭代器
实现,而lambda
底层实则是仿函数对象
,即我们编写一个 lambda 后,编译器会自动生成一个对应的仿函数类。
- 仿函数的类名由编译器按特定规则生成,确保不同 lambda 生成的类名互不相同
- lambda 的
参数
—> 对应仿函数operator()的参数
- lambda 的
返回类型与函数体
—> 对应仿函数返回类型与函数体
- lambda 的
捕获列表
—> 本质是生成的仿函数类的成员变量
也就是说,捕获列表里的变量会作为 lambda 类构造函数的实参,对于隐式捕获,编译器会依据实际使用的变量来传递相应对象。
关于上述原理,我们可通过汇编层进一步了解,下方的汇编代码可印证这些原理。
代码案例:根据lambda表达式和仿函数的关系阐述lambda表达式的本质
#include <iostream>
using namespace std;// ====================== 仿函数类:Rate ======================
class Rate
{
private:double _rate; // 存储利率的成员变量public://1.实现:“构造函数”Rate(double rate): _rate(rate) // 用传入的利率初始化成员变量{ }//2.实现:“重载 operator()”double operator()(double money, int year) //目的:实现计算逻辑{//功能:计算利息 = 本金 * 利率 * 年限return money * _rate * year;}
};int main()
{//1.定义利率(后续用于仿函数和 lambda)double rate = 0.49;// ====================== lambda 表达式 ======================auto func1 = [rate](double money, int year){return money * rate * year;};/*---------------------- 调用“仿函数对象”----------------------*///1.首先用“仿函数类rate”初始化“仿函数对象func2”Rate func2(rate);//2.然后调用仿函数cout << func2(10000, 2) << endl; //计算 10000 本金,2 年的利息/*---------------------- 调用“lambda对象”----------------------*///1.调用lambda对象:cout << func1(10000, 2) << endl; //计算 10000 本金,2 年的利息return 0;
}
lambda 的底层是仿函数类:
“匿名仿函数类” 与 “显示仿函数类Rate” 的对比:
// 手动定义的仿函数类
class Rate { ... }; // 编译器为 lambda 生成的匿名类(逻辑等价)
class __lambda_xxxx
{
private:double _rate; // 对应捕获的 rate
public://1.实现:“构造函数”__lambda_xxxx(double rate) : _rate(rate) {} //2.实现:“重载 operator()”double operator()(double money, int year) { return money * _rate * year; }
};
总结:“lambda” 与 “仿函数” 的关系
特性 | 手动定义的仿函数(Rate) | 编译器生成的 lambda 类 |
---|---|---|
类名 | 显式定义(Rate) | 匿名(由编译器生成唯一名称) |
成员变量 | 手动声明(_rate) | 自动捕获外部变量 |
operator () 逻辑 | 手动实现 | 由 lambda 函数体自动生成 |
调用方式 | 对象调用(func2 (...) ) | 同仿函数(func1 (...) ) |
lambda表达式的本质:
lambda 是 “语法糖”,底层完全基于仿函数实现
编写 lambda 时,编译器会自动生成等价的仿函数类,简化了手动定义仿函数的繁琐过程
------------ 包装器 ------------
1. 什么是包装器?
包装器(Wrapper)
:是一种能对其他对象、函数、数据等进行封装的机制,目的是简化使用、统一接口或添加额外功能
- 它就像给原本的东西套了一层 “壳”,让操作更方便
- 常见的包装器有
std::function
、std::bind
以及智能指针
(某种意义上也算资源包装器 )
包装器的核心作用:
- 统一接口:把不同类型的可调用对象(函数指针、仿函数、lambda 等)“包装” 成相同类型,方便存储、传递。
- 比如:lambda 是匿名类型,直接用很难统一管理,但
std::function
能把它们包装成一致的类型- 简化使用:隐藏底层复杂细节,调用时不用关心被包装对象的具体类型,按统一方式调用即可。
- 扩展功能:可以在包装器里添加额外逻辑(比如:日志、异常处理 ),被包装的对象无需修改。
2. 常见的包装器有哪些?
常见的包装器有:
- 函数包装器(如:
std::function
)- 绑定器(如:
std::bind
)- 智能指针(如:
std::unique_ptr
、std::shared_ptr
)
2.1:函数包装器(std::function)
C++ 中通过类模板实现
std::function
,核心原型简化如下:(标准库简化)
// 前向声明基础模板(未定义具体逻辑)
template <class T>
class function; // 特化模板:用于匹配“返回值为 Ret、参数为 Args...”的可调用对象
template <class Ret, class... Args>
class function<Ret(Args...)>;
std::function
:是 C++ 标准库<functional>
里的类模板
,专门用来包装可调用对象(函数指针
、仿函数
、lambda
、std::bind结果
等 ),使它们具有统一的调用方式,从而方便存储、传递和管理。
std::function 是可调用对象的 “包装器”,关键特性:
- 支持可调用对象:
能包裹函数指针、仿函数(函数对象)、lambda 表达式、std::bind
绑定的表达式等- “空” 状态与异常:
若std::function
未绑定任何可调用对象(空状态 ),调用时会抛出std::bad_function_call
异常- 统一类型抽象:
无论原始可调用对象类型多复杂(如:lambda 是匿名类型 ),都能被std::function
包装为相同类型,便于声明和使用
std::function的类型声明:
function<int(int, int)>
- 这里
std::function<int(int, int)>
就是 “包装器类型”- 模板参数
<int(int, int)>
表示:返回值为int
,接受两个int
参数的可调用对象- 可简化理解为:
function<返回类型(参数类型列表)>
意义:不管底层是函数指针、仿函数还是 lambda,都被包装成相同类型,调用方式完全一致。
代码示例1:用
std::function
统一包装不同可调用对象
#include <iostream>
#include <functional>
using namespace std; //1.实现:两数相加的“普通函数”
int add(int a, int b)
{return a + b;
}//2.实现:两数相乘的“仿函数”(函数对象)
struct Multiplier
{//重载函数调用运算符,使对象可像函数一样调用int operator()(int a, int b) const {return a * b;}
};int main()
{// ==================== std::function 包装不同类型的可调用对象 ====================//1.包装普通函数:将函数 add 包装为 std::function 类型function<int(int, int)> func1 = add;//2.包装仿函数:实例化 Multiplier 对象并包装function<int(int, int)> func2 = Multiplier();//3.包装lambda表达式:将匿名lambda函数包装为 std::function 类型function<int(int, int)> func3 = [](int a, int b) {return a - b;};// ==================== 统一调用方式,无需关心底层实现 ====================//1.调用“普通函数包装器”cout << "func1(3, 4) = " << func1(3, 4) << endl; // 输出 7(3+4)//2.调用“仿函数包装器”cout << "func2(3, 4) = " << func2(3, 4) << endl; // 输出 12(3*4)//3.调用“lambda包装器”cout << "func3(3, 4) = " << func3(3, 4) << endl; // 输出 -1(3-4)return 0;
}
不同可调用对象的不同包装方式:
普通函数:直接使用
函数名
(函数指针)仿函数:需要先
实例化对象
(Multiplier()
),再进行包装lambda表达式:直接使用
匿名lambda函数
// ==================== std::function 包装不同类型的可调用对象 ====================
//1.包装普通函数:将函数 add 包装为 std::function 类型
function<int(int, int)> func1 = add;//2.包装仿函数:实例化 Multiplier 对象并包装
function<int(int, int)> func2 = Multiplier();//3.包装lambda表达式:将匿名lambda函数包装为 std::function 类型
function<int(int, int)> func3 = [](int a, int b){return a - b;};
不同可调用对象的统一调用语法:
func1(3, 4); // 无论底层是函数、仿函数还是 lambda,调用方式完全一致
意义:体现了
std::function
的核心价值:类型擦除,统一接口
std::function
内部通过类型擦除技术,封装了不同类型的可调用对象,对外提供统一的调用接口std::function
存在一定的性能开销(虚函数调用),对于性能敏感场景,可考虑使用函数指针或模板
代码示例2:用
std::function
包装“静态成员函数”和“普通成员函数”
#include<iostream>
#include<functional>
using namespace std;//实现:包含成员函数的类 Plus
class Plus
{
private:/*-----------------成员变量-----------------*/int _n; // 成员变量,影响 plusd 的计算结果public://1.实现:“构造函数”Plus(int n = 10): _n(n){ }//2.实现:“静态成员函数”static int staticFunc(int a, int b){return a + b; //(无依赖对象状态)}//3.实现:“普通成员函数”double Func(double a, double b){return (a + b) * _n; //(依赖对象的 _n 值)}
};int main()
{// ====================== 1. 包装“静态成员函数” ====================== /********************* 静态成员函数无隐含 this 指针,直接取地址即可 *********************/function<int(int, int)> f4 = &Plus::staticFunc;cout << f4(1, 1) << endl; // 等价于调用 Plus::staticFunc(1,1),结果 2// ====================== 2. 包装“普通成员函数” ======================/********************* 普通成员函数有隐含 this 指针,需指定“对象”或“对象指针”调用 *********************/Plus pd; // 创建 Plus 对象//1.用对象指针绑定(this 指针为 &pd)function<double(Plus*, double, double)> f5 = &Plus::Func;cout << f5(&pd, 1.1, 1.1) << endl; // 调用:pd.Func(1.1, 1.1) → (1.1+1.1)*10(_n 默认为 10)//2.用对象值绑定(this 指针为 pd 的拷贝)function<double(Plus, double, double)> f6 = &Plus::Func;cout << f6(pd, 1.1, 1.1) << endl; //调用:pd.Func(1.1, 1.1) → 同样 (2.2)*10//3.用右值引用绑定(this 指针为临时对象)function<double(Plus&&, double, double)> f7 = &Plus::Func;//3.1:move(pd)将 pd 转为右值,调用临时对象的 Funccout << f7(move(pd), 1.1, 1.1) << endl;//3.2:直接构造临时对象 Plus(),调用其 Funccout << f7(Plus(), 1.1, 1.1) << endl;return 0;
}
2.2:绑定器(std::bind)
std::bind
有两种典型声明形式:(标准库简化)
// 形式 1:自动推导返回类型
template <class Fn, class... Args>
auto bind(Fn&& fn, Args&&... args); // 形式 2:显式指定返回类型(较少直接使用)
template <class Ret, class Fn, class... Args>
auto bind(Fn&& fn, Args&&... args);
std::bind
:是定义在<functional>
头文件中的函数模板
,它的主要作用是对函数
或可调用对象的参数
进行绑定、重新排列,生成一个新的可调用对象。
std::bind 像一个可调用对象的 “函数适配器”,支持:
- 固定参数:把可调用对象的某些参数 “固定” 为具体值,减少调用时传递的参数。
- 调整参数顺序:重新排列可调用对象的参数顺序。
- 参数占位:用占位符(如:
_1
、_2
)预留参数位置,让新的可调用对象能接收动态参数。
std::bind的类型声明:
// 调用格式: auto newCallable = bind(callable, arg_list); // 实际运用: auto newFunc = bind(multiply, 2, placeholders::_1, placeholders::_2);
- 第一个参数:
callable
:必须是被包装的可调用对象(如:multiply
)- 后续参数:
arg_list
:参数列表,支持两种元素
- 具体值:(如:
2
)会固定 callable 对应参数的位置(这里固定a=2
,不清楚的话可以先看一下下面的代码)- 占位符:(如:
_1
、_2
)(来自std::placeholders
命名空间 ),表示新可调用对象 newCallable 的参数位置- 赋值符左侧的:
newCallable
:返回的新可调用对象,调用它时,会触发原始callable
执行,并传递arg_list
中配置的参数(包括占位符对应的动态参数 )总结:
std::bind
的第一个参数是要绑定的可调用对象,后续参数可以是具体的值用于绑定原始函数的参数,也可以是占位符std::placeholders::_n
,占位符表示新函数的参数位置。
代码案例:使用
std::bind
绑定器生成一个新的可调用对象
#include <iostream>
#include <functional>
using namespace std; //实现:三数相乘的“原始函数”
int multiply(int a, int b, int c)
{return a * b * c;
}int main()
{// ==================== 使用 std::bind 绑定参数 ====================auto newFunc = bind(multiply, 2, placeholders::_1, placeholders::_2);/* bind 函数的作用:创建一个新的可调用对象,固定原函数的某些参数* 语法:std::bind(原函数, 参数1, 参数2, ...)* 这里:* 1. 将 multiply 的第一个参数固定为 2* 2. 使用占位符 _1 和 _2 表示新函数的第1和第2个参数* 3. 占位符来自命名空间 std::placeholders,因 using 语句可直接使用*/// ==================== 调用绑定后的函数 ====================cout << "newFunc(3, 4) = " << newFunc(3, 4) << endl; // 输出 2*3*4=24/* 调用 newFunc(3, 4) 时:* 3会被传递给占位符 _1,对应 multiply 的第二个参数* 4会被传递给占位符 _2,对应 multiply 的第三个参数* 等价于调用 multiply(2, 3, 4)*/return 0;
}
占位符的作用:
占位符
std::placeholders::_n
:表示新函数的参数位置。(n
表示参数位置)
placeholders::_1
,placeholders::_2
, …:是std::placeholders
命名空间中的对象
placeholders::_1
,placeholders::_2
, … :用于指定新函数的参数如何映射到原函数的参数位置
- 例如:
_1
表示新函数的第一个参数,在调用时会被传递给原函数对应的位置
新函数的调用逻辑:
newFunc(3, 4);
- 绑定时:
a=2
(固定值),b
和c
由占位符指定- 调用时:
3
和4
分别填充到占位符位置,最终执行2*3*4
调用
newFunc(3, 4);
时,参数传递路径为:3 → _1 → multiply 的第二个参数 4 → _2 → multiply 的第三个参数
调用
newFunc(3, 4);
等价于调用multiply(2, 3, 4)
std::bind扩展说明:
多参数绑定
// 固定 multiply 的第二个参数为 3 auto func = bind(multiply, placeholders::_1, 3, placeholders::_2);func(2, 4); // 等价于 multiply(2, 3, 4)
参数重排序
// 交换原函数的第二个和第三个参数 auto swapped = bind(multiply, placeholders::_1, placeholders::_3, placeholders::_2); //注:swapped是bind包装multiply后返回的新可调用对象swapped(2, 3, 4); // 等价于 multiply(2, 4, 3)
绑定成员函数
#include <iostream> #include <functional> using namespace std;//定义一个包含成员函数的类 struct Calculator {//实现:计算两个整数的和的“成员函数”int add(int a, int b){return a + b;} };int main() {//1.创建 Calculator 类的实例Calculator calc;// ==================== 使用 std::bind 绑定成员函数 ====================//2.绑定成员函数auto boundAdd = bind(&Calculator::add, &calc, placeholders::_1, placeholders::_2);/* 绑定语法:* std::bind(成员函数指针, 对象指针, 参数1, 参数2, ...)** 这里:* 1. &Calculator::add:成员函数的地址(必须使用 & 取地址)* 2. &calc:对象的指针(绑定到哪个对象实例)* 3. placeholders::_1, placeholders::_2:新函数的第1和第2个参数*/// ==================== 调用绑定后的函数对象 ====================//3.调用绑定后的函数对象cout << "boundAdd(3, 4) = " << boundAdd(3, 4) << endl; // 输出 7/* 调用 boundAdd(3, 4) 时:* 3 和 4 分别填充到占位符 _1 和 _2 的位置* 等价于调用 calc.add(3, 4)*/return 0; }
成员函数绑定的特殊要求:
bind(&Calculator::add, &calc, ...)
- 必须使用&取成员函数地址:
&Calculator::add
是正确写法- 必须传递对象指针:
&calc
指定成员函数要作用于哪个对象实例- 若 Calculator::add 是 const 成员函数,则需传递 const Calculator*
绑定成员函数与普通函数的区别:
特性 | 普通函数绑定 | 成员函数绑定 |
---|---|---|
第一个参数 | 函数名(自动转换为函数指针) | 必须显式使用 &类名::成员函数名 |
是否需要对象指针 | 否 | 必须(指定调用该成员函数的对象) |
示例 | bind(func, _1, _2) | bind(&Class::method, &obj, _1) |
代码示例:绑定器 bind 的使用大总结
#include <iostream>
#include <functional>
#include <utility> // 用于支持 move 语义
using namespace std; // 引入占位符 _1、_2、_3,简化 bind 中参数占位的写法
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;//1.实现:两参数减法运算,结果放大 10 倍
int Sub(int a, int b)
{return (a - b) * 10;
}//2.实现:三参数减法运算,结果放大 10 倍
int SubX(int a, int b, int c)
{return (a - b - c) * 10;
}//3.类 Plus,包含“静态成员函数”和“普通成员函数”
class Plus
{
public://3.1:静态成员函数:两整数的加法(静态函数属于类,无需对象即可调用)static int plusi(int a, int b){return a + b;}//3.2:普通成员函数:两浮点数的加法(需要通过对象调用,隐含 this 指针)double plusd(double a, double b){return a + b;}
};int main()
{// ========== 1. 基础用法:绑定函数参数,保持参数顺序 ==========auto sub1 = bind(Sub, _1, _2);cout << sub1(10, 5) << endl; //等价于直接调用 Sub(10, 5),计算 (10 - 5) * 10 = 50// ========== 2. 调整参数顺序(展示 bind 灵活调整参数的能力) ==========auto sub2 = bind(Sub, _2, _1);cout << sub2(10, 5) << endl; //等价于 Sub(5, 10),计算 (5 - 10) * 10 = -50// ========== 3. 固定部分参数(常用场景:简化调用,减少重复传参) ==========//1. 固定 Sub 的第一个参数为 100auto sub3 = bind(Sub, 100, _1);cout << sub3(5) << endl; //等价于 Sub(100, 5),计算 (100 - 5) * 10 = 950//2. 固定 Sub 的第二个参数为 100auto sub4 = bind(Sub, _1, 100);cout << sub4(5) << endl; //等价于 Sub(5, 100),计算 (5 - 100) * 10 = -950// ========== 4. 绑定类成员函数(区分静态和普通成员函数) ==========// 方式 1:使用“对象”绑定类Plus的“普通成员函数”plusd//1.创建类Plus的对象实例Plus pd;//2.使用“对象”绑定类Plus的“普通成员函数”function<double(Plus, double, double)> f6 = &Plus::plusd; //绑定“普通成员函数”,需传入对象实例才能调用(因为依赖 this 指针)//3.通过“对象”调用cout << f6(pd, 1.1, 1.1) << endl; //通过 pd 对象调用 plusd(1.1, 1.1),结果 1.1 + 1.1 = 2.2//4.通过“临时对象”调用cout << f6(move(Plus()), 1.1, 1.1) << endl; //用临时对象调用,move 优化(也可直接传 Plus(),效果一样,这里展示 move 用法)// 方式 2:使用 bind 绑定成员函数plusd + 固定对象实例Plus()function<double(double, double)> f7 = bind(&Plus::plusd, Plus(), _1, _2); //绑定到临时 Plus 对象,调用时传 _1、_2 对应 plusd 的两个参数cout << f7(1.1, 1.1) << endl; // 等价于 Plus().plusd(1.1, 1.1),结果 2.2return 0;
}
类成员函数绑定(f6、f7)
- 绑定普通成员函数时,必须传入对象实例(pd 或临时对象 Plus()),因为成员函数隐含 this 指针
- 用 bind 可以 “固定对象实例”,让后续调用更简洁(如 :f7 直接传两个 double 即可)
3. 包装器怎么应用?
3.1:std::function的应用
1. 可调用对象的 “映射表”
- 用
std::map
建立 “字符串 → 可调用对象” 的映射,通过std::function
统一类型。std::function 的作用:统一包装不同的 lambda 表达式(比如:加法、减法等)等,使它们能存储在 map 中,实现 “运算符 → 逻辑” 的映射。
#include <iostream>
#include <functional>
#include <map>
#include <string>
using namespace std;int main()
{//1.定义一个映射表(map),用于存储"名称→函数逻辑"的对应关系/* 说明:* 1. 键(key)类型:string(函数的名称标识)* 2. 值(value)类型:function<int(int)>(可调用对象包装器,接受int参数并返回int)*/ map<string, function<int(int)>> funcMap ={//1.1:向映射表中添加第一个键值对:{"double", [](int x) { return x * 2; }}, //键为"double",值为一个lambda表达式(匿名函数)//1.2:向映射表中添加第二个键值对:{"addFive", [](int x) { return x + 5; }}, //键为"addFive",值为另一个lambda表达式};//2.调用映射表中的函数逻辑://2.1:通过键"double"从映射表中获取对应的函数,传入参数3并执行auto result1 = funcMap["double"](3);cout << "调用 double(3) 的结果: " << result1 << endl;//2.2:通过键"addFive"从映射表中获取对应的函数,传入参数3并执行auto result2 = funcMap["addFive"](3);cout << "调用 addFive(3) 的结果: " << result2 << endl;return 0;
}
实践案例1:150. 逆波兰表达式求值
方法一:方传统方式的实现
/*-------------------------- 传统方式的实现 --------------------------*/
class Solution
{
public:int evalRPN(vector<string>& tokens){//1.定义一个栈,用于存储操作数stack<int> stk;//2.遍历 tokens 中的每个元素for (auto& str : tokens){//2.1:如果当前遍历到的字符是“运算符”if (str == "+" || str == "-" || str == "*" || str == "/"){//第一步:先栈顶取出第二个操作数(后入栈的操作数,即右操作数)int right = stk.top();stk.pop();//第二步;再栈顶取出第一个操作数(先入栈的操作数,即左操作数)int left = stk.top();stk.pop();//第三步:根据运算符类型,执行对应的计算switch (str[0]){case '+':stk.push(left + right);break;case '-':stk.push(left - right);break;case '*':stk.push(left * right);break;case '/':stk.push(left / right);break;}}//2.2:如果当前遍历到的字符是“操作数”else{//1.将其转换为 int 后入栈stk.push(stoi(str)); // stoi(str):将字符串形式的操作数转换为 int 类型,方便入栈计算}}//3.栈顶元素即为最终结果return stk.top();}
};
方法二:map映射实现
/*-------------------------- 使用 map 映射 string 和 function 的方式实现 --------------------------*/
class Solution
{
public:int evalRPN(vector<string>& tokens) {//1.定义一个栈,用于存储操作数stack<int> stk;//2.定义一个 map,用于映射运算符和对应的计算函数/* 说明:* 1. key: 运算符(如 "+"、"-"、"*"、"/")* 2. value: function<int(int, int)> 类型的可调用对象,接收两个 int 参数,返回一个 int 结果*/map<string, function<int(int, int)>> opFuncMap = {//2.1:加法运算{"+", [](int x, int y) { return x + y; }},//2.2:减法运算{"-", [](int x, int y) { return x - y; }},//2.3:乘法运算{"*", [](int x, int y) { return x * y; }},//2.4:除法运算{"/", [](int x, int y) { return x / y; }}}; //3.遍历 tokens 中的每个元素for (auto& str : tokens) {//3.1:如果当前遍历到的字符是“运算符”if (opFuncMap.count(str)) {//第一步:先栈顶取出第二个操作数(后入栈的操作数,即右操作数)int right = stk.top();stk.pop();//第二步;后栈顶取出第一个操作数(先入栈的操作数,即左操作数)int left = stk.top();stk.pop();//第三步:根据运算符,调用对应的计算函数,并将结果入栈int ret = opFuncMap[str](left, right);stk.push(ret);}//3.2:如果当前遍历到的字符是“操作数”else {//1.将其转换为 int 后入栈stk.push(stoi(str)); // stoi(str):将字符串形式的操作数转换为 int 类型,方便入栈计算}}//4.栈顶元素即为最终结果return stk.top();}
};
传统方式实现 VS map映射实现:
传统方式实现:
- 核心逻辑:
使用stack
存储操作数,遍历tokens
时,遇到运算符则从栈中弹出两个操作数,根据运算符类型(switch-case
判断)执行计算,结果重新入栈;遇到操作数则转换为int
后入栈。- 优缺点:
- 优点:
逻辑直观
,适合简单的四则运算场景- 缺点:新增运算符时,需要修改 switch-case 分支,
扩展性较差
map映射实现:
- 核心逻辑:
利用std::map
建立运算符 → 计算函数的映射关系,遍历tokens
时,遇到运算符则从map
中获取对应的计算函数,执行运算;遇到操作数则转换为int
后入栈。- 优缺点:
- 优点:
扩展性强
,新增运算符时只需在map
中添加映射关系,无需修改核心逻辑;代码更简洁
,利用 std::function 和 lambda 表达式简化函数调用- 缺点:
map 的查找效率为 O(log n)
(n
为运算符数量),理论上略低于 switch-case 的 O(1)O(1)O(1),但实际场景中差异可忽略
2. 与 lambda/仿函数的配合
- 因为
std::function
能包裹lambda
,所以可简化代码中“函数对象类型不明确” 的问题
#include <iostream>
#include <functional>
using namespace std; int main()
{//1.将lambda表达式赋值给std::function对象function<void()> printHello = [] {cout << "Hello, std::function!" << endl;};/* 说明:* 1. function<void()> 表示:包装一个无参数、无返回值的可调用对象* 2. lambda表达式 []{} :无捕获列表,无参数,函数体为输出一句话*///2.调用function对象,本质是执行包装的lambda表达式printHello(); return 0;
}
综上:
std::function 是 C++ 中管理可调用对象的 “胶水层”,通过统一类型包装,让函数指针、lambda 等能更方便地协同工作,尤其在复杂场景(如:回调管理、策略模式 )中优势显著。
3.2:std::bind的应用
1. 与 lambda/仿函数的配合
#include <iostream>
#include <functional>
using namespace std;
using namespace std::placeholders; //引入占位符命名空间,让 _1、_2 等直接可用int main()
{// ========== 结合 bind + lambda,实现复杂逻辑(复利计算场景) ==========//1.定义lanbda表达式:计算复利利息auto func1 = [](double rate, double money, int year) -> double {//1.定义变量记录总金额double ret = money;//2.复利公式:每年利息加入本金,循环 year 次for (int i = 0; i < year; ++i){ret += ret * rate;}//3.返回总利息(最终金额 - 本金)return ret - money;};//2.固定利率、年数,只让本金作为可变参数,生成专用函数// 场景 1:利率 3.5%,3 年,不同本金 → 调用时只需传本金function<double(double)> func3_3_5 = bind(func1, 0.035, _1, 3);// 场景 2:利率 1.5%,5 年function<double(double)> func5_1_5 = bind(func1, 0.015, _1, 5);// 场景 3:利率 2.5%,10 年function<double(double)> func10_2_5 = bind(func1, 0.025, _1, 10);// 场景 4:利率 3.5%,30 年function<double(double)> func30_3_5 = bind(func1, 0.035, _1, 30);//3.测试:本金 1000000,计算各场景利息cout << "场景 1(3.5%利率,3年)利息:" << func3_3_5(1000000) << endl;cout << "场景 2(1.5%利率,5年)利息:" << func5_1_5(1000000) << endl;cout << "场景 3(2.5%利率,10年)利息:" << func10_2_5(1000000) << endl;cout << "场景 4(3.5%利率,30年)利息:" << func30_3_5(1000000) << endl;return 0;
}
std::bind 的价值总结:
- 解耦参数传递:无需修改原始函数,就能调整参数的传递逻辑。
- 适配复杂调用:在回调函数、策略模式中,灵活调整参数,让接口更通用。
- 简化重复调用:固定常用参数,减少重复传参的冗余代码。
简单说:
std::bind
是 C++ 中灵活处理 “可调用对象参数” 的工具,通过包装和适配,让函数调用更灵活、更贴合复杂场景~