news 2026/2/22 12:02:26

C++11 -- lambda、包装器和可变参数模板

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++11 -- lambda、包装器和可变参数模板

目录

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 = 50

auto sub2 = bind(Sub, _2, _1); cout << sub2(10, 5) << endl; // 输出: (5-10)*10 = -50
  • sub2(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 = -950
  • sub3(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.2
  • bind固定了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. 递归函数模板:处理至少一个参数的情况

  2. 递归终止函数:处理参数包为空的情况

# 调用时,本质上编译器将可变参数模板通过模式的包扩展,编译器推导的几个重载函数函数:

# 还可以借助逗号表达式来展开参数包:

// 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),大幅简化包扩展的写法,无需递归即可展开参数包。具体细节以后再介绍。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/19 9:01:13

同样是技术岗,35 岁后为何网络安全行业越老越吃香?

前几天我表弟小王来找我喝茶&#xff0c;聊着聊着突然问我&#xff1a;“老曹&#xff0c;你说我要不要转行做网络安全啊&#xff1f; 听说这行业挺赚钱的。 “我一听就笑了&#xff0c;这不正好最近我刚研究过这个行业吗&#xff1f; 我跟他说&#xff0c;别看现在各行各业…

作者头像 李华
网站建设 2026/1/30 9:25:40

[特殊字符] uni-app App 端实现文件上传功能(基于 xe-upload 插件)

在 uni-app 开发中&#xff0c;文件上传是一个常见且重要的功能。尤其是在 App 端&#xff0c;如何实现一个既美观又实用的文件上传与展示界面&#xff0c;是很多开发者关心的问题。本文将介绍如何通过 xe-upload 插件&#xff0c;结合自定义 UI&#xff0c;实现一个完整的文件…

作者头像 李华
网站建设 2026/2/20 21:32:38

二、Visual Studio 2026如何创建C语言项目

1.打开软件2.创建新项目3.下一步4.创建5.添加源文件&#xff08;1&#xff09;新建项&#xff08;2&#xff09;显示所有模板&#xff08;3&#xff09;添加C语言源文件后缀名为.c6.添加头文件&#xff08;1&#xff09;新建项&#xff08;2&#xff09;显示所有模板&#xff0…

作者头像 李华
网站建设 2026/2/16 19:27:08

Java二叉树:原理、实现与实战

深入浅出Java二叉树&#xff1a;原理、实现与实战 一、二叉树核心概念深度解析 1. 二叉树的定义与分类 二叉树是一种每个节点最多有2个子节点的树状结构&#xff0c;子节点分为左子节点&#xff08;lChild&#xff09;和右子节点&#xff08;rChild&#xff09;。根据节点分布规…

作者头像 李华
网站建设 2026/2/21 10:33:08

动态规划(四)算法设计与分析 国科大

0-1背包问题输入&#xff1a;给定物品集合 &#xff0c;每个物品 i 对应重量 和价值&#xff1b;同时给定背包的总重量限制 W。输出&#xff1a;选择物品的一个子集&#xff0c;满足 “子集总重量不超过 W” 的约束&#xff0c;同时最大化子集的总价值。这是一个二元决策问题&a…

作者头像 李华
网站建设 2026/2/11 16:49:37

为什么90%的团队搞不定云原生Agent部署?Docker批量方案深度拆解

第一章&#xff1a;云原生Agent部署的现状与挑战随着云原生技术的快速发展&#xff0c;Agent作为实现可观测性、自动化运维和安全监控的核心组件&#xff0c;被广泛部署于Kubernetes集群、边缘节点及混合云环境中。这些轻量级代理程序负责采集指标、日志和追踪数据&#xff0c;…

作者头像 李华