目录
1、lambda表达式
1.1 语法
1.2 捕捉列表
1.3 原理
1.4 捕捉列表 VS 函数参数
1.5 Lambda 在实际开发中的典型应用
2、包装器
2.1 function
2.2 bind
3、可变参数模板
3.1 概念与使用
3.2 包扩展
1、lambda表达式
1.1 语法
# Lambda 表达式本质上是一个匿名函数对象,与普通函数不同之处在于它可以定义在函数内部,从语法层面看,Lambda 表达式没有显式类型,因此通常使用 auto 或模板参数来接收 Lambda 对象。
# Lambda 表达式的基本格式:
[capture-list] (parameters)-> return-type { function-body }
- [ capture-list ] : 捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据 [ ] 来判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下文中的变量供 lambda 函数使用,捕捉列表可以传值和传引用捕捉,具体细节后文再细讲。捕捉列表为空也不能省略。
- (parameters) :参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连同 () ⼀起省略
- -> return type :返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。⼀般返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
- { function boby } :函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略。
int main() { // ⼀个简单的lambda表达式 auto add1 = [](int x, int y)->{return x + y; }; cout << add1(1, 2) << endl; // 1、捕捉为空也不能省略 // 2、参数为空可以省略 // 3、返回值可以省略,可以通过返回对象⾃动推导 // 4、函数体不能省略 auto func1 = [] { cout << "hello bit" << endl; return 0; }; //func1(); int a = 0, b = 1; auto swap1 = [](int& x, int& y) { int tmp = x; x = y; y = tmp; }; swap1(a, b); //cout << a << ":" << b << endl; return 0; }#include<vector> using namespace std; struct Goods { string _name; // 名字 double _price; // 价格 int _evaluate; // 评价 // ... Goods(const char* str, double price, int evaluate) :_name(str) , _price(price) , _evaluate(evaluate) {} }; struct ComparePriceLess { bool operator()(const Goods& gl, const Goods& gr) { return gl._price < gr._price; } }; struct ComparePriceGreater { bool operator()(const Goods& gl, const Goods& gr) { return gl._price > gr._price; } }; int main() { vector<Goods> v = { { "苹果", 2.1, 5 }, { "⾹蕉", 3, 4 }, { "橙⼦", 2.2, 3 }, { "菠萝", 1.5, 4 } }; // 类似这样的场景,我们实现仿函数对象或者函数指针⽀持商品中 // 不同项的⽐较,相对还是⽐较⿇烦的,那么这⾥lambda就很好⽤了 sort(v.begin(), v.end(), ComparePriceLess()); sort(v.begin(), v.end(), ComparePriceGreater()); sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) { return g1._price < g2._price; }); sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) { return g1._price > g2._price; }); sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) { return g1._evaluate < g2._evaluate; }); sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) { return g1._evaluate > g2._evaluate; }); return 0; }1.2 捕捉列表
# lambda 表达式默认只能访问函数体和参数中的变量,如需使用外层作用域的变量,需通过捕获列表实现。
# 显式捕获方式分为值捕获和引用捕获,多个捕获变量用逗号分隔。例如:[ x, y, &z ] 表示对 x 和 y 进行值捕获,对 z 进行引用捕获
int x = 0; // 捕捉列表必须为空,因为全局变量不用捕捉就可以用,没有可被捕捉的变量 auto func1 = []() { x++; }; int main() { // 只能用当前lambda局部域和捕捉的对象和全局对象 int a = 0, b = 1, c = 2, d = 3; auto func1 = [a, &b] { // 值捕捉的变量不能修改,引用捕捉的变量可以修改 //a++; // 错误,不能修改 b++; int ret = a + b; return ret; }; cout << func1() << endl; return 0; }#隐式捕捉方式通过在捕获列表中使用符号自动捕获变量:
- "=" 表示对所有使用变量进行值捕获
- "&" 表示对所有使用变量进行引用捕获
int main() { // 隐式值捕捉 // 用了哪些变量就捕捉哪些变量 auto func2 = [=] { int ret = a + b + c; return ret; }; cout << func2() << endl; // 隐式引用捕捉 // 用了哪些变量就捕捉哪些变量 auto func3 = [&] { a++; c++; d++; }; func3(); cout << a << " " << b << " " << c << " " << d << endl; }#混合捕捉方式允许同时使用显式和隐式捕获:
- [ =, &x ] 表示其他变量值捕获,x 引用捕获
- [ &, x, y ] 表示其他变量引用捕获,x 和 y 值捕获
- 混合捕获时,第一个元素必须是 "&" 或 "=",且后续捕获类型需与之相反
int main() { // 混合捕捉1 auto func4 = [&, a, b] { //a++; //b++; c++; d++; return a + b + c + d; }; func4(); cout << a << " " << b << " " << c << " " << d << endl; // 混合捕捉1 auto func5 = [=, &a, &b] { a++; b++; /*c++; d++;*/ return a + b + c + d; }; return 0; }# 局部作用域中的 lambda 表达式:
- 只能捕获其定义位置之前的变量
- 不能捕获静态局部变量和全局变量(这些变量可直接使用)
- 全局定义的 lambda 表达式捕获列表必须为空
int x = 0; // 捕捉列表必须为空,因为全局变量不用捕捉就可以用,没有可被捕捉的变量 auto func1 = []() { x++; }; int main() { // 只能用当前lambda局部域和捕捉的对象和全局对象 int a = 0, b = 1, c = 2, d = 3; auto func1 = [a, &b] { // 值捕捉的变量不能修改,引用捕捉的变量可以修改 //a++; // 错误,不能修改 b++; int ret = a + b; return ret; }; cout << func1() << endl; // 局部的静态和全局变量不能捕捉,也不需要捕捉 static int m = 0; auto func6 = [] { int ret = x + m; return ret; }; return 0; }# 捕获变量默认具有 const 属性:
- 值捕获的变量不可修改
- 使用 mutable 修饰符可取消 const 属性(此时参数列表不可省略)
- 修改仅作用于形参,不影响实参
int main() { // 传值捕捉本质是一种拷⻉,并且被const修饰了 // mutable相当于去掉const属性,可以修改了 // 但是修改了不会影响外面被捕捉的值,因为是一种拷⻉ auto func7 = [=]()mutable { a++; b++; c++; d++; return a + b + c + d; }; cout << func7() << endl; cout << a << " " << b << " " << c << " " << d << endl; return 0; }1.3 原理
# lambda 的原理和范围 for 很像,编译后从汇编指令层的角度看,压根就没有 lambda 和范围 for 这样的东西。范围 for 底层是迭代器,而lambda 底层是仿函数对象,也就说我们写了⼀个lambda 以后,编译器会生成⼀个对应的仿函数的类。
# 仿函数的类名是编译按⼀定规则( UUID 算法)生成的,保证不同的 lambda生成的类名不同,lambda 的参数/返回类型/函数体,就是仿函数 operator() 的参数/返回类型/函数体, lambda 的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参,当然隐式捕捉,编译器要看使用哪些就传那些对象。
class Rate { public: Rate(double rate) : _rate(rate) {} double operator()(double money, int year) { return money * _rate * year; } private: double _rate; }; int main() { double rate = 0.49; // lambda auto r2 = [rate](double money, int year) { return money * rate * year; }; // 函数对象 Rate r1(rate); r1(10000, 2); r2(10000, 2); auto func1 = [] { cout << "hello world" << endl; }; func1(); return 0; }# 上面的原理,我们可以透过汇编层了解⼀下:
1.4 捕捉列表 VS 函数参数
# Lambda 表达式的捕捉列表(Capture List)和函数参数是完全不同的设计,核心解决「访问函数外部变量」的问题 —— 直接传参只能处理 “调用 Lambda 时传入的临时值”,而捕捉列表才能让 Lambda 访问定义它时所在作用域的外部变量(如局部变量、类成员),二者无法互相替代。
1.5 Lambda 在实际开发中的典型应用
多线程编程
优势:
直接捕获任务相关参数
逻辑封装在任务启动点
避免定义额外函数
智能指针自定义删除器
// 1. 定义Lambda作为文件删除器:接管FILE*的销毁逻辑 auto file_deleter = [](FILE* f) { if (f) { // 防御性检查:避免空指针调用fclose(fclose(NULL)未定义) std::cout << "Closing file\n"; fclose(f); // 关闭文件,释放系统资源 } }; // 2. 定义unique_ptr,绑定FILE*和自定义删除器 std::unique_ptr<FILE, decltype(file_deleter)> file_ptr(fopen("data.txt", "r"), file_deleter);对比传统方式:
// 传统函数指针方式 void file_deleter_func(FILE* f) { /*...*/ } std::unique_ptr<FILE, void(*)(FILE*)> ptr(fopen(...), &file_deleter_func);异步任务封装
// 提交异步任务 auto future = std::async(std::launch::async, [url = "https://example.com"] { return fetch_data(url); // 捕获URL }); // 获取结果 auto result = future.get();GUI 事件处理
// 按钮点击事件处理 button.on_click([&counter, this](const Event& e) { counter++; // 捕获计数器引用 update_display(); // 捕获this指针 log_event(e); // 访问事件对象 });回调函数简化
// 传统C风格回调 void register_callback(void (*callback)(int, void*), void* data); // 使用lambda封装状态 int state = 42; register_callback([](int result, void* data) { int* s = static_cast<int*>(data); std::cout << "Result: " << result << ", State: " << *s; }, &state); // C++11后更优雅的方式 std::function<void(int)> callback = [&state](int result) { std::cout << "Result: " << result << ", State: " << state; }; register_cpp_callback(callback);2、包装器
2.1 function
# std::function 是一个通用的函数包装器模板类,属于 C++11标准库的一部分。它提供了一种类型安全的方式来存储、复制和调用各种可调用对象(callable objects)。参考文档: std::function - cppreference.com
# std::function 的实例对象可以包装存储其他的可以调用对象,包括函数指针、仿函数、 lambda 、bind 表达式等,存储的可调用对象被称为 std::function 的目标。若 std::function 不含目标,则称它为空。调用空 std::function 的目标导致抛出 std::bad_function_call 异常。
# 基本用法如下:
std::function<int(int, int)> func; // 声明一个接受两个int参数,返回int的function对象
func = [](int a, int b){ return a + b; }; // 包装lambda表达式
int result = func(2, 3); // 调用存储的可调用对象
# 函数指针、仿函数、 lambda 等可调用对象的类型各不相同, std::function 的优势就是统⼀类型,对他们都可以进行包装,这样在很多地方就方便声明可调用对象的类型。
// 1. 普通函数 int f(int a, int b) { return a + b; } // 2. 仿函数(函数对象):重载operator() struct Functor { int operator() (int a, int b) { return a + b; } }; // 3. 类(含静态/非静态成员函数) class Plus { public: Plus(int n = 10) :_n(n) {} // 构造函数初始化成员变量_n // 静态成员函数:无this指针 static int plusi(int a, int b) { return a + b; } // 非静态成员函数:隐含this指针,依赖对象实例 double plusd(double a, double b) { return (a + b) * _n; } private: int _n; // 非静态成员,仅对象实例可访问 };2.2 bind
# bind 是一个功能强大的函数模板,属于可调用对象包装器范畴。它本质上是一个函数适配器,也是一个仿函数,允许开发者对输入的可调用对象 fn 进行参数绑定和调整,最终返回一个新的可调用对象。bind 的主要功能包括:
- 调整参数个数(减少或重新排列)
- 改变参数顺序
- 绑定部分参数值
- 使用占位符表示未绑定参数
# 该模板位于 <functional> 头文件中,是标准库提供的重要功能组件。
# 典型的 bind 调用形式如下:
auto newCallable = bind(callable, arg_list);- newCallable 是生成的新可调用对象
- callable 是原始可调用对象(函数、函数指针、成员函数、函数对象等)
- arg_list 是以逗号分隔的参数列表,对应 callable 的参数
# 当调用 newCallable 时,它会自动调用原始 callable,并传递 arg_list 中绑定的参数。
# arg_list 中的参数可能包含形如 _n 的名字,其中 n 是⼀个整数,这些参数是占位符,表示 newCallable 的参数,它们占据了传递给 newCallable 的参数的位置。数值 n 表示生成的可调用对象中参数的位置:_1 为 newCallable 的第⼀个参数,_2 为第⼆个参数,以此类推。_1/_2/_3… 这些占位符放到一个名为 std::placeholders 的命名空间中,使用时需要:
using namespace std::placeholders; // 或者 using std::placeholders::_1; using std::placeholders::_2; // ...# 调整参数顺序:
auto sub1 = bind(Sub, _1, _2); cout << sub1(10, 5) << endl; // 输出: (10-5)*10 = 50auto sub2 = bind(Sub, _2, _1); cout << sub2(10, 5) << endl; // 输出: (5-10)*10 = -50sub2(10, 5)→Sub(5, 10)_2对应第一个实参10,_1对应第二个实参5
# 多参数绑定:
auto sub3 = bind(Sub, 100, _1); cout << sub3(5) << endl; // 输出: (100-5)*10 = 950 auto sub4 = bind(Sub, _1, 100); cout << sub4(5) << endl; // 输出: (5-100)*10 = -950sub3(5)→Sub(100, 5)sub4(5)→Sub(5, 100)
auto sub5 = bind(SubX, 100, _1, _2); cout << sub5(5, 1) << endl; // 输出: (100-5-1)*10 = 940 auto sub6 = bind(SubX, _1, 100, _2); cout << sub6(5, 1) << endl; // 输出: (5-100-1)*10 = -960 auto sub7 = bind(SubX, _1, _2, 100); cout << sub7(5, 1) << endl; // 输出: (5-1-100)*10 = -960分别固定第1、2、3个参数
剩余参数通过占位符传递
# 成员函数绑定:
1.直接使用std::function
function<double(Plus&&, double, double)> f6 = &Plus::plusd;函数类型:
double(Plus&&, double, double)需要传递一个
Plus对象作为第一个参数
Plus pd; cout << f6(move(pd), 1.1, 1.1) << endl; // 输出: 2.2 cout << f6(Plus(), 1.1, 1.1) << endl; // 输出: 2.2传递左值对象(使用
std::move)传递右值对象(临时对象)
2. 使用std::bind绑定对象
function<double(double, double)> f7 = bind(&Plus::plusd, Plus(), _1, _2); cout << f7(1.1, 1.1) << endl; // 输出: 2.2bind固定了Plus对象(临时对象)创建只需两个参数的新函数对象
3、可变参数模板
3.1 概念与使用
# C++11 支持可变参数模板,允许定义带有可变数量参数的函数模板和类模板。可变参数被称为参数包,主要分为两类:
- 模板参数包:表示零个或多个模板参数
- 函数参数包:表示零个或多个函数参数
template <class... Args> void Func(Args... args) {} template <class... Args> void Func(Args&... args) {} template <class... Args> void Func(Args&&... args) {}- 使用省略号(
...)表示参数包- 在模板参数列表中,
class...或typename...表示零个或多个类型参数 - 在函数参数列表中,类型名后的
...表示零个或多个形参
- 在模板参数列表中,
- 函数参数包可以:
- 使用左值引用 (
&) 表示 - 使用右值引用 (
&&) 表示 - 遵循模板实例化时的引用折叠规则
- 使用左值引用 (
实现原理
- 可变参数模板的实现机制与普通模板类似
- 本质上是通过实例化生成对应类型和参数数量的多个函数
# 可以使用sizeof...运算符可获取参数包中的参数数量。
template <class ...Args> void Print(Args&&... args) { cout << sizeof...(args) << endl; } int main() { double x = 2.2; Print(); // 包里有0个参数 Print(1); // 包里有1个参数 Print(1, string("xxxxx")); // 包里有2个参数 Print(1.1, string("xxxxx"), x); // 包里有3个参数 return 0; }3.2 包扩展
# 对于参数包,除了计算其参数数量外,唯一能做的操作就是扩展它。扩展参数包时,需要为每个元素指定应用模式。扩展的本质是将包分解为构成元素,对每个元素应用给定模式,从而获得扩展后的列表。
# 通过在表达式右侧添加...操作符来触发扩展操作:
# C++ 还支持更高级的包扩展方式,可以直接将参数包中的元素依次展开作为实参传递给函数进行处理。
# 特别注意:不支持使用 arg[i] 这样的方式来获取参数包中的参数,因为可变参数模板是在编译时解析,而不是在运行时解析的,如下代码:
template <class ...Args> void Print(Args... args) { // 可变参数模板是在编译时解析 // 下面是运行获取和解析,所以不支持这样用 cout << sizeof...(args) << endl; for (size_t i = 0; i < sizeof...(args); i++) { cout << args[i] << " "; } cout << endl; }# 一般使用递归扩展来获取参数包中的参数,如下代码:
void ShowList() { // 编译器时递归的终止条件,参数包是0个时,直接匹配这个函数 cout << endl; } template <class T, class ...Args> void ShowList(T x, Args... args) { cout << x << " "; // args是N个参数的参数包 // 调用ShowList,参数包的第一个传给x,剩下N-1传给第二个参数包 ShowList(args...); } // 编译时递归推导解析参数 template <class ...Args> void Print(Args... args) { ShowList(args...); } int main() { Print(); Print(1); Print(1, string("xxxxx")); Print(1, string("xxxxx"), 2.2); return 0; }# 递归展开包含两个关键部分:
递归函数模板:处理至少一个参数的情况
递归终止函数:处理参数包为空的情况
# 调用时,本质上编译器将可变参数模板通过模式的包扩展,编译器推导的几个重载函数函数:
# 还可以借助逗号表达式来展开参数包:
// 1. 单个参数处理函数:打印参数并返回引用 template <class T> const T& GetArg(const T& x) { cout << x << " "; // 核心逻辑:打印单个参数 return x; // 返回引用(保证能组成参数包传递) } // 2. 空可变参数函数:仅用于接收展开后的参数包(无实际逻辑) template <class ...Args> void Arguments(Args... args) {} // 3. 核心可变参数打印函数:展开参数包并调用GetArg template <class ...Args> void Print(Args... args) { // 包扩展:GetArg(args)... 等价于 GetArg(1), GetArg("xxxxx"), GetArg(2.2) // 扩展后的多个返回值,作为Arguments的参数包传入 Arguments(GetArg(args)...); } int main() { Print(1, string("xxxxx"), 2.2); return 0; }# C++17 引入折叠表达式(Fold Expression),大幅简化包扩展的写法,无需递归即可展开参数包。具体细节以后再介绍。