前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >【C++11】闭包:仿函数operator() && 绑定器bind && 包装器functino && lambda表达式

【C++11】闭包:仿函数operator() && 绑定器bind && 包装器functino && lambda表达式

作者头像
利刃大大
发布2025-04-15 13:28:27
发布2025-04-15 13:28:27
12700
代码可运行
举报
文章被收录于专栏:csdn文章搬运csdn文章搬运
运行总次数:0
代码可运行

Ⅰ. 什么是闭包

闭包有很多种定义, 一种说法是, 闭包是带有上下文的函数。 说白了, 就是有状态的函数,就是有自己的变量。更直接一些, 就是一个类 换了个名字而已。

一个函数, 带上了一个状态, 就变成了闭包了。 那什么叫 “带上状态” 呢? 意思是这个闭包有属于自己的变量, 这些个变量的值是创建闭包的时候设置的, 并在调用闭包的时候, 可以访问这些变量。

函数是代码, 状态是一组变量, 将代码和一组变量捆绑 (bind) , 就形成了闭包。 闭包的状态捆绑, 必须发生在运行时。

Ⅱ. 闭包的实现方式

一、仿函数:重载operator()

​ 之前我们在学 std::list 等容器模拟实现的时候讲到了仿函数,其实就是用一个类,在类中重载 operator() 即可达到类似函数的调用方式,本质上就是一个重载 () 的函数

代码语言:javascript
代码运行次数:0
运行
复制
#include<iostream>

class MyFunctor 
{
public:
    MyFunctor(int tmp)
        :round_(tmp)
    {}
    int operator()(int tmp) // 仿函数,重载operator()
    {
        return round_ + tmp;
    }
private:
    int round_;  // round_就是这个闭包的状态
};

int main() 
{
    int round = 2;
    MyFunctor mf(round);   // 调用构造函数

    // 调用仿函数
    std::cout << "result = " << mf(1) << std::endl;  // operator()(int tmp)
    return 0;
}

// 运行结果:
result = 3

二、绑定器与包装器

C++ 11中的std::bind和std::function std::function与std::bind使用总结 包装器和绑定器std::bind和std::function的回调技术

C++ 中函数指针的用途非常广泛,例如回调函数,接口类的设计等,但函数指针始终不太灵活,它只能指向全局或静态函数,对于类成员函数、lambda表达式或其他可调用对象就无能为力了,因此,C++11 推出了 std::functionstd::bind 这两件大杀器。

① std::function包装器

官方文档

C++ 中, 可调用实体主要包括:函数、函数指针、函数引用、可以隐式转换为函数指定的对象,或者实现了 opetator() 的对象。

​ 而 C++11 中, 新增加了一个 std::function 类模板, 它是对 C++ 中现有的可调用实体的一种类型安全的包裹。 通过指定它的模板参数, 它可以用统一的方式处理函数、 函数对象、 函数指针,并允许保存和延迟执行它们。

std::function 对象最大的用处就是在实现函数回调,使用者需要注意,虽然它不能被用来检查相等或者不相等,但是可以与 NULL 或者 nullptr 进行比较。

​ 💥💥💥当然,任何东西都会有优缺点,std::function 填补了函数指针的灵活性,但会对调用性能有一定损耗,经测试发现,在调用次数达10亿次时,函数指针比直接调用要慢 2 秒左右,而 std::function 要比函数指针慢 2 秒左右,这么少的损耗如果是对于调用次数并不高的函数,替换成 std::function 绝对是划得来的。

代码语言:javascript
代码运行次数:0
运行
复制
#include <functional>
template <class T> function;     	// undefined

template <class Ret, class... Args>
class function<Ret(Args...)>;

/*
	模板参数说明:
	Ret: 被调用函数的返回类型
	Args…:被调用函数的形参
*/

​ 下面我们写一段代码来测试一下它的功能!

​ 测试一:(代码中的宏 __LINE__ 代表的是当前所在的行数,__func__ 表示的是当前调用的是哪一个函数。)

代码语言:javascript
代码运行次数:0
运行
复制
#include<iostream>
#include<functional>
// 1、普通函数
void Func() 
{
    std::cout << __LINE__ << ":" << __func__ << std::endl;
}
// 2、类中静态函数
class Test 
{
public:
    static int Func(int tmp) 
    {
        std::cout << __LINE__ << ":" << __func__ << "(" << tmp << ")->:";
        return tmp;
    }
};
// 3、类中仿函数
class MyFunctor {
public:
    MyFunctor(int tmp)
        :round_(tmp)
    {}
    int operator()(int tmp) 
    {
        std::cout << __LINE__ << ":" << __func__ << "(" << tmp << ")->:";
        return tmp + round_;
    }
private:
    int round_;
};
int main() 
{
    // 1、绑定普通函数
    std::function<void(void)> f1 = Func;
    f1(); // 等价于 func()

    // 2、绑定类中的静态函数,需要使用 类名::函数 的方式进行绑定
    std::function<int(int)> f2 = Test::Func;
    std::cout << f2(10) << std::endl; // Test::Func(10)

    //3、绑定类中的仿函数,绑定对象,仿函数调用obj
    MyFunctor obj(100);
    std::function<int(int)> f3 = obj;
    std::cout << f3(20) << std::endl;
    return 0;
}

// 运行结果
158:Func
166:Func(10)->:10
178:operator()(20)->:120

​ 测试二:

代码语言:javascript
代码运行次数:0
运行
复制
typedef std::function<void()> PrintFinFunction; // 将包装器重命名一下
// using PrintFinFunction = function<void()>;   // 也可以用c++11的using来重命名包装器

// 回调函数
void print(const char* text, PrintFinFunction callback) 
{
    printf("%s\n", text);
    if (callback) // 不为空则去回调
        callback();
}

// test1普通函数
void printFinCallback() 
{
    cout << "Normal callback" << endl;
}
// test2类静态函数
class Test 
{
public:
    static void printFinCallback() {
        cout << "Static callback" << endl;
    }
};
// test3仿函数,重载()运算符
struct Functor 
{
    void operator() () {
        cout << "Functor callback" << endl;
    }
};
int main()
{
    print("test 1", printFinCallback);
    print("test 2", Test::printFinCallback); // 类静态函数需要用 类名::函数名 来传递参数去绑定
    print("test 3", Functor()); 		 	 // 仿函数要创建对象
    print("test 4", []() {
        cout << "Lambda callback" << endl;
        });
}

// 运行结果
test 1
Normal callback
test 2
Static callback
test 3
Functor callback
test 4
Lambda callback

​ 总的来说,function 包装器就是用来实现对函数指针、仿函数、lambda表达式等进行统一的表示,我们可以看看下面的例子就能看到一些问题:

代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>
#include <functional>
using namespace std;

template<class F, class T>
T useF(F f, T x)
{
	static int count = 0;
	cout << "count:" << ++count << endl;
	cout << "count:" << &count << endl;
	return f(x);
}
// 普通函数
double f(double i)
{
	return i / 2;
}
// 仿函数
struct Functor 
{
	double operator()(double d)
	{
		return d / 3;
	}
};

int main()
{
	// 函数名
	cout << useF(f, 11.11) << endl;
	// 函数对象
	cout << useF(Functor(), 11.11) << endl;
	// lambda表达式
	cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
	return 0;
}

// 运行结果:
count:1
count:00007FF607272574
5.555
count:1
count:00007FF607272578
3.70333
count:1
count:00007FF60727257C
2.7775

​ 可以很明显的发现,程序中我们用 useF 函数模板来作为一个中转函数,分别去调用普通函数、仿函数、lambda表达式,但是它们的 count 改变的时候,其实对各自是没有影响的,并且地址都是不同的,这说明什么❓❓❓

说明 useF 函数模板被实例化为了三份不同的函数,这其实就导致了没必要的内存开销等等!

​ 那么这个问题我们就可以通过包装器 function 来解决,包装器就是为了统一我们调用这些函数的类型,也就是说,如果我们用 function 去包装我们调用的函数,那么 useF 函数模板怎么识别都是只有一个类型也就是 function 类型,那么这样子就只会实例化一份函数,大大的减少了开销!

​ 下面是使用 function 和 不使用 function 的区别:

代码语言:javascript
代码运行次数:0
运行
复制
int main()
{
	// 函数名
	cout << useF(f, 11.11) << endl;
	// 函数对象
	cout << useF(Functor(), 11.11) << endl;
	// lamber表达式
	cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
	cout << "################################################################################" << endl;

	// 函数名
	function<double(double)> func1 = f;
	cout << useF(func1, 11.11) << endl;
    // cout << useF(function<double(double)>(f), 11.11) << endl;  // 也可以写成这样子
    
	// 函数对象
	function<double(double)> func2 = Functor();
	cout << useF(func2, 11.11) << endl;
    
	// lamber表达式
	function<double(double)> func3 = [](double d)->double { return d / 4; };
	cout << useF(func3, 11.11) << endl;
	return 0;
}

// 运行结果:
count:1
count:00007FF607272574
5.555
count:1
count:00007FF607272578
3.70333
count:1
count:00007FF60727257C
2.7775
################################################################################
count:1
count:00007FF607272580
5.555
count:2
count:00007FF607272580
3.70333
count:3
count:00007FF607272580
2.7775

​ 明显发现,我们的三份函数就是同一份!

💥除此之外,这里还要强调一些使用包装器的时候一些细节:

我们在绑定仿函数的时候,需要绑定的是一个对象,而不是这个仿函数的类名。

代码语言:javascript
代码运行次数:0
运行
复制
struct Functor
{
public:
	int operator() (int a, int b)
	{
		return a + b;
	}
};
int main()
{
    function<int(int, int)> func1 = Functor;   // ❌错误
    function<int(int, int)> func2 = Functor(); // 👍正确
	cout << func2(1, 2) << endl;
    return 0;
}

一般在绑定仿函数的时候,我们不能通过()来进行初始化赋值,而要通过 = 号来进行赋值,这可能是编译器在识别 ()内定义的函数对象把这个函数对象识别成了函数指针,导致我们如果调用了通过()初始化的 function 对象的时候,编译器会报错(vs 编译器、gcc编译器都会报错),而对于普通函数等,通过()来初始化绑定 function 是可以通过编译并且正常运行的!,比如下面的代码:

代码语言:javascript
代码运行次数:0
运行
复制
struct Functor
{
public:
	int operator() (int a, int b)
	{
		return a + b;
	}
};
int main()
{
	std::function<int(int, int)> func1(Functor());
    cout << func1(1, 2) << endl;  // ❌错误
    
	std::function<int(int, int)> func2 = Functor();
	cout << func2(1, 2) << endl;  // 👍正确
	return 0;
}

对于类的非静态成员函数,我们在绑定的时候需要特别处理:

  1. 因为类的非静态成员函数其中的第一个参数其实是该类对象的 this 指针,所以 我们在绑定的时候第一个参数必须是该类的类名,注意不是传递该类对象的指针因为 this 指针其实是不允许我们显式去调用的,所以这里语法规定我们需要传一个该类的类名来占位。
  2. 并且我们在 调用包装器对象的时候,第一个参数需要传递一个该类的对象,可以允许是个临时对象。
  3. 除此之外,我们在绑定这个成员函数的时候,必须通过 &类名::成员函数 ,前面这个 & 是不能省略的因为我们需要的是一个指向成员的指针,所以必须取地址!

对于类的静态成员函数,会相对简单一点,因为它是没有 this 指针的,只不过我们还是需要通过 &类名::成员函数 来进行绑定,其中不同的是静态成员函数可以不必加 &,但是为了防止与非静态成员函数必须加 & 的语法规定混淆,我们推荐静态成员函数一样要加 &

代码语言:javascript
代码运行次数:0
运行
复制
class Plus
{
public:
	static int static_add(int a, int b)
	{
		return a + b;
	}
	double add(double a, double b)
	{
		return a + b;
	}
};

int main()
{
	// 静态成员函数
	function<int(int, int)> func1 = &Plus::static_add;
	cout << func1(1, 2) << endl;

	// 非静态成员函数
	function<double(Plus, double, double)> func2 = &Plus::add; // 第一个参数用类名来占位表示this指针
	cout << func2(Plus(), 1.1, 2.2) << endl; // 第一个参数传递要传一个类的对象

	// 如果不想非静态成员函数这样子调用加上对象来占位的话,可以使用lambda表达式来捕捉(或者通过下面讲的绑定器来实现)
	Plus plus;
	function<double(double, double)> func3 = [&plus](double x, double y)->double { return plus.add(x, y); };
	cout << func3(1.1, 2.2) << endl;
	return 0;
}

包装器的其他一些场景题目:

代码语言:javascript
代码运行次数:0
运行
复制
class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        // 利用包装器和lambda,映射运算符与函数的关系
        map<string, function<int(int, int)>> hash = {
            {"+", [](int a, int b)->int{ return a + b; }},
            {"-", [](int a, int b)->int{ return a - b; }},
            {"*", [](int a, int b)->int{ return a * b; }},
            {"/", [](int a, int b)->int{ return a / b; }}
        };

        stack<int> st;
        for(int i= 0; i < tokens.size(); ++i)
        {
            if(hash.count(tokens[i]) == 0) // 说明不是运算符
            {
                st.push(stoi(tokens[i]));
            }
            else
            {
                int right = st.top();
                st.pop();
                int left = st.top();
                st.pop();
                st.push(hash[tokens[i]](left, right));
            }
        }
        return st.top();
    }
};
② std::bind绑定器

std::bind 函数定义在头文件 <functional> 中,是一个函数模板,一般而言,我们用它可以把一个原本接收 N 个参数的函数 fn,通过绑定一些参数,返回一个接收 M 个(M可以大于N,但这么做没什么意义)参数的新函数。同时,使用 std::bind 函数还可以实现参数顺序调整等操作。

std::bind 可以预先把指定可调用实体的某些参数绑定到已有的变量, 产生一个新的可调用实体, 这种机制在回调函数的使用过程中也颇为有用。

C++98 中,有两个函数 bind1stbind2nd,它们分别可以用来绑定 functor 的第一个和第二个参数,它们都是只可以绑定一个参数,由于各种限制,使得 bind1stbind2nd 的可用性大大降低。

​ 在 C++11 中, 提供了 std::bind它绑定的参数的个数不受限制, 绑定的具体哪些参数也不受限制, 由用户指定, 这个 bind 才是真正意义上的绑定。

代码语言:javascript
代码运行次数:0
运行
复制
// 原型如下:
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);

// with return type (2) 
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
在这里插入图片描述
在这里插入图片描述

​ 可以将 bind 函数看作是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来 “适应” 原对象的参数列表。

​ 调用 bind 的一般形式:auto newCallable = bind(callable, arg_list);

​ 其中,newCallable 本身是一个可调用对象,arg_list 是一个逗号分隔的参数列表,对应给定的 callable 的参数。当我们调用 newCallable 时, newCallable 会调用 callable, 并传给它 arg_list 中的参数

arg_list 中的参数可能包含形如 _n 的名字,其中 n 是一个整数,这些参数是 “占位符”,表示 newCallable 的参数,它们占据了传递给 newCallable 的参数的“位置”。数值 n 表示生成的可调用对象中参数的位置:_1newCallable 的第一个参数,_2 为第二个参数,以此类推……

​ 下面我们写一段代码来测试一下它的功能!

代码语言:javascript
代码运行次数:0
运行
复制
#include<iostream>
#include<functional>

void Func(int x, int y)
{
    std::cout << x << " " << y << std::endl;
}

int main() {
    std::bind(Func, 10, 20)(30, 40); // 输出10,20

    std::bind(Func, std::placeholders::_1, std::placeholders::_2)(11, 21, 31);  // 输出11,21

    using namespace std::placeholders; // 下面引入std::placeholders,节省代码量

    std::bind(Func, _1, 11)(10, 20, 30); // 输出10,11

    std::bind(Func, _1, _2)(11, 21);   // 输出11,21

    std::bind(Func, _2, _1)(12, 22);   // 先输出_2就是22,在输出_1就是12

    // std::bind(Func,_2,22)(11);  // ❌参数不匹配,没有第二个参数

    auto b1 = std::bind(Func, _2, 22);
    b1(11, 0); // 输出0,22
	cout << typeid(b1).name() << endl;
    
    auto b2 = std::bind(Func, _3, 22);
    b2(0, 1, 2); // 输出3,22

    return 0;
}

// 运行结果
10 20
11 21
10 11
11 21
22 12
0 22
class std::_Binder<struct std::_Unforced,void (__cdecl&)(int,int),struct std::_Ph<2> const & __ptr64,int>
2 22

​ 其中 std::placeholders::_1 是一个占位符, 代表这个位置将在函数调用时, 被传入的第一个参数所替代。

③ std::bind 和 std::function 的配合使用

​ 刚才也说道,std::function 可以指向类成员函数和函数签名不一样的函数,其实,这两种函数都是一样的,因为类成员函数都有一个默认的参数:this,作为第一个参数,这就导致了类成员函数不能直接赋值给 std::function,这时候我们就需要 std::bind 了!

​ 简言之,std::bind 的作用就是转换函数签名,将缺少的参数补上,将多了的参数去掉,甚至还可以交换原来函数参数的位置

代码语言:javascript
代码运行次数:0
运行
复制
#include<iostream>
#include<functional>
using namespace std;
typedef std::function<void(int)> PrintFinFunction; // 将包装器重命名

void print(const char *text, PrintFinFunction callback) 
{
    printf("%s\n", text);
    if (callback) // 不为空则去回调
        callback(1);
}
// 类成员函数
class Test 
{
public:
    void printFinCallbackInter(int res) 
    {
        cout << "Class Inter callback" << endl;
    }
};
// 函数签名不一样的函数
void printFinCallback2(int res1, int res2) 
{
    cout << "Different callback " << res1 << " " << res2 << endl;
}

int main()
{
    Test testObj;
    
    function<void(int)> callback5 = bind(&Test::printFinCallbackInter, testObj, placeholders::_1); // 函数模板只有一个参数,这里需要补充this参数
    print("test 5", callback5); 
    
    // 也可以用auto省略function的类型
    auto callback6 = bind(&printFinCallback2, placeholders::_1, 100); // 这里需要补充第二个参数
    print("test 6", callback6); 
    
    // 也可以直接调用
    print("test 7", bind(&printFinCallback2, placeholders::_1, 200));
}

// 运行结果:
test 5
Class Inter callback:1
test 6
Different callback:1 100
test 7
Different callback:1 200

​ 从上面的代码中可以看到,std::bind 的用法就是第一个参数是要被指向的函数的地址,为了区分,这里 std::bind 语句的左值函数为原函数,右值函数为新函数,那么 std::bind 方法从第二个参数起,都是新函数所需要的参数,缺一不可,而我们可以使用 std::placeholders::_1std::placeholders::_2 等等占位符来使用原函数的参数,_1 就是原函数的第一个参数,如此类推。

值得注意的有两点:

  • 一旦 bind 补充了缺失的参数,那么以后每次调用这个 function 时,那些原本缺失的参数都是一样的,举个栗子,上面代码中 callback6,我们每次调用它的时候,第二个参数都只会是 100
  • 正因为第一点,所以假如我们是在 iOS 程序中使用 std::bind 传入一个缺省参数,那么我们转化后的那个 function 会持有那些缺省参数,这里我们需要防止出现循环引用导致内存泄漏。

通过 std::bindstd::function 配合使用, 所有的可调用对象均有了统一的操作方法。

代码语言:javascript
代码运行次数:0
运行
复制
// 更加简约的实例
#include <iostream>
#include <functional>
using namespace std;
int Plus(int a, int b)
{
	return a + b;
}
class Sub
{
public:
	int sub(int a, int b)
	{
		return a - b;
	}
};
int main()
{
	// 表示绑定函数plus参数分别由调用func1的第一、二个参数指定
	function<int(int, int)> func1 = bind(Plus, placeholders::_1, placeholders::_2);
	cout << func1(1, 2) << endl;

	// func2的类型为function<int(int, int)>与func1类型一样
	// 表示绑定函数 plus 的第一,二为:1, 2
	auto func2 = bind(Plus, 1, 2);
	cout << func2() << endl;

	Sub s;
	// func3绑定成员函数,无需添加类名和lei'du来占位
	function<int(int, int)> func3 = bind(&Sub::sub, s, placeholders::_1, placeholders::_2);
    
	// func4与func3的参数调换顺序
	function<int(int, int)> func4 = bind(&Sub::sub, s, placeholders::_2, placeholders::_1);
	cout << func3(1, 2) << endl;
	cout << func4(1, 2) << endl;
    
    // 利用包装器和绑定器将less比较器改成greater比较器,只需要用绑定器调整参数位置即可
    function<bool(int, int)> func5 = bind(less<int>(), _1, _2);
	function<bool(int, int)> func6 = bind(less<int>(), _2, _1);
	cout << func5(1, 2) << endl;
	cout << func6(1, 2) << endl;
	return 0;
}

// 运行结果:
3
3
-1
1
1
0

三、lambda表达式

lambda 表达式可以看下一个部分的内容,专门讲 lambda 表达式!

Ⅲ. lambda表达式

一、问题的引入

​ 引入 lambda 表达式其实是为了解决一些比较棘手的问题比如要排序的数据是自定义类型的话,需要用户定义排序时的比较规则,那么以前我们是有两种方法的,第一种就是函数指针,第二种就是仿函数

代码语言:javascript
代码运行次数:0
运行
复制
struct Goods
{
    string _name;
    double _price;
    Goods(const char* str, double price)
        :_name(str)
        ,_price(price)
    {}
};
// 仿函数
struct Compare
{
    bool operator()(const Goods& gl, const Goods& gr)
    {
        return gl._price <= gr._price;
    }
};
int main()
{
    vector<Goods> gds= { { "苹果",2.1 }, { "相交",3 }, { "橙子",2.2 }, {"菠萝",1.5} };
    sort(gds.begin(), gds.end(), Compare());
    return 0; 
}

​ 随着 C++ 语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个 algorithm 算法, 都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在 C++11 语法中出现了 lambda 表达式。

二、lambda表达式

代码语言:javascript
代码运行次数:0
运行
复制
int main()
{
    vector<Goods> gds= { { "苹果",2.1 }, { "相交",3 }, { "橙子",2.2 }, {"菠萝",1.5} };
    sort(gds.begin(), gds.end(), [](const Goods& l, const Goods& r)->bool
         							{ return l._price < r._price; });
    return 0; 
}

​ 上述代码就是使用 C++11 中的 lambda 表达式来解决,可以看出 lambda 表达式本质是一个匿名函数

三、lambda表达式语法

​ 格式如下:

代码语言:javascript
代码运行次数:0
运行
复制
[capture-list] (parameters) mutable -> return-type { statement }

其中各部分说明:

  • [capture-list] : 捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据 [] 来判断接下来的代码是否为 lambda 函数捕捉列表能够捕捉上下文中的变量供 lambda 函数使用注意只能捕捉当前作用域中的变量!
  • (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
  • mutable:默认情况下,lambda 函数总是一个 const 函数,mutable 可以取消其常量性。使用该修饰符时参数列表不可省略(即使参数为空)。
  • ->returntype :返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导
  • {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。

注意:lambda 函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空

​ 因此 C++11 中最简单的 lambda 函数为: []{}; ,虽说该 lambda 函数不能做任何事情,没有意义。

🐰 下面我们先来写一些简单的 lambda 表达式:

代码语言:javascript
代码运行次数:0
运行
复制
int main()
{
	// 最简单的lambda表达式, 该lambda表达式没有任何意义
	[]{};

	int a = 3, b = 11;
	// 实现两个数相加的lambda表达式(返回值也可以省略,让编译器推导)
	auto add1 = [](int x, int y)->int{ return x + y; };
	cout << add1(a, b) << endl;

	// 若想定义和add1一样的变量,则有两种方法:auto或者decltype
	auto add2 = add1;
	decltype(add1) add3 = add1;
	cout << typeid(add2).name() << endl;
	cout << typeid(add3).name() << endl;

	return 0;
}

//运行结果
14
class `int __cdecl main(void)'::`2'::<lambda_2>
class `int __cdecl main(void)'::`2'::<lambda_2>

​ 那么这个 捕捉列表mutable 是干什么的呢?我们平时交换两个数可能是这样子写的:

代码语言:javascript
代码运行次数:0
运行
复制
int main()
{
	int a = 10, b = 20;

	// 两数交换的lambda表达式
	auto swap = [](int& x, int& y) {
		int z = x;
		x = y;
		y = z;
	};
	swap(a, b);
	cout << a << " " << b << endl;
	return 0;
}

//运行结果
20 10

这里要注意的一个点就是要传引用或者指针接收,不然的话传值是无法达到交换的效果的,和函数是一样的!

​ 但是对于 lambda 表达式来说就不需要再这么复杂了,因为有了捕捉列表:

代码语言:javascript
代码运行次数:0
运行
复制
int main()
{
	int a = 10, b = 20;

	// 两数交换的lambda表达式
	auto swap = [a, b]{
		int c = a;
		a = b;
		b = c;
	};
	swap(a, b);
	cout << a << " " << b << endl;
	return 0;
}

​ 但是这里就有问题了,编译器报错,说函数体中的 ab 必须是可以修改的左值,也就是说他们的属性是 const,那么这个时候 mutable 就派上用场了:

代码语言:javascript
代码运行次数:0
运行
复制
int main()
{
	int a = 10, b = 20;

	// 两数交换的lambda表达式
	auto swap = [&a, &b]()mutable{
		int c = a;
		a = b;
		b = c;
	};
	swap();
	cout << a << " " << b << endl;
	return 0;
}

​ 除此之外,这里要达到交换 ab,必须要 传引用捕捉它们!并且还可以发现当我们传引用去捕捉后,我们是可以不加 mutable 的,因为我们捕捉的其实是地址,所以可以改变

​ 还有要注意的是 使用 mutable 的话,不能省略其前面的参数列表

捕获列表说明:捕捉列表描述了上下文中那些数据可以被 lambda 使用,以及使用的方式传值还是传引用。

  • [var]:表示值传递方式捕捉变量 var
  • [=]:表示 值传递 方式捕获所有父作用域中的变量(包括this
  • [&var]:表示引用传递捕捉变量 var
  • [&]:表示 引用传递 捕捉所有父作用域中的变量(包括this
  • [this]:表示值传递方式捕捉当前的 this 指针

注意

  1. 父作用域指包含 lambda 函数的语句块
  2. 语法上捕捉列表 可由多个捕捉项组成,并以逗号分割
    • 比如:[=, &a, &b]:以引用传递的方式捕捉变量 ab,值传递方式捕捉其他所有变量。而 [&,a, this] 表示值传递方式捕捉变量 athis,引用方式捕捉其他变量 。
  3. 捕捉列表 不允许变量重复传递,否则就会导致编译错误。 比如:[=, a]= 已经以值传递方式捕捉了所有变量,捕捉 a 重复。
  4. 在块作用域以外的 lambda 函数捕捉列表必须为空。
  5. 在块作用域中的 lambda 函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错
  6. lambda 表达式之间不能相互赋值,即使看起来类型相同,但可以将 lambda 表达式赋值给相同类型的函数指针。
代码语言:javascript
代码运行次数:0
运行
复制
void (*PF)();

int f = 10, g = 100;
// 全局的lambda捕捉不了变量
// auto cat1 = [&a, &b] {};
// auto cat2 = [f, g] {};
	auto cat3 = [] {cout << "just only empty!" << endl; };

int main()
{
	int a = 1, b = 2, c = 3;
	auto change1 = [&, b] {
		a = 10;
		//b = 20; b无法被修改因为是传值接收的,而其余变量是传引用接收
		c = 30;
	};
	change1();
	cout << a << " " << b << " " << c << endl;

	auto f1 = [] {cout << "hello world" << endl; };
	auto f2 = [] {cout << "hello world" << endl; };
	// 此处先不解释原因,等lambda表达式底层实现原理看完后,大家就清楚了
	// f1 = f2; // 编译失败--->提示找不到operator=()
	// 允许使用一个lambda表达式拷贝构造一个新的副本
	auto f3(f2);
	f3();

	// 可以将lambda表达式赋值给相同类型的函数指针
	PF = f2;
	PF();

	// 调用全局中的lambda
	cat3();

	// 不能捕捉重复属性的!
	// auto func1 = [=, a]() {};
	// auto func2 = [&, &a]() {};

	return 0;
}

//运行结果
10 2 30
hello world
hello world
just only empty!

四、函数对象与lambda表达式

​ 函数对象,又称为仿函数,即可以想函数一样使用的对象,就是在类中重载了 operator() 运算符的类对象。

代码语言:javascript
代码运行次数:0
运行
复制
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;
	Rate r1(rate);
	cout << r1(10000, 2) << endl;;

	// lamber
	auto r2 = [=](double money, int year)->double { return money * rate * year; };
	cout << r2(10000, 2) << endl;
	return 0;
}

// 运行结果
9800
9800

​ 从使用方式上来看,函数对象与 lambda 表达式完全一样。

​ 函数对象将 rate 作为其成员变量,在定义对象时给出初始值即可,lambda 表达式通过捕获列表可以直接将该变量捕获到。

​ 实际在底层编译器对于 lambda 表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个 lambda 表达式,编译器会自动生成一个类,在该类中重载了 operator()

底层原理:其实是被处理成一个 lambda_uuid 的一个仿函数类

​ 这就好比与 范围for 的本质其实就是迭代器,而 lambda 表达式 的本质就是 仿函数

UUID科普

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-04-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Ⅰ. 什么是闭包
  • Ⅱ. 闭包的实现方式
    • 一、仿函数:重载operator()
    • 二、绑定器与包装器
      • ① std::function包装器
      • ② std::bind绑定器
      • ③ std::bind 和 std::function 的配合使用
    • 三、lambda表达式
  • Ⅲ. lambda表达式
    • 一、问题的引入
    • 二、lambda表达式
    • 三、lambda表达式语法
    • 四、函数对象与lambda表达式
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档