C++11(下)(可变参数模版、lambda表达式及包装器)
文章目录
一、C++11新增的类功能
默认成员函数
原来C++的类中,有6个默认成员函数:
1.构造函数
2.析构函数
3.拷贝构造函数
4.拷贝赋值重载
5.取地址重载
6.const 取地址重载
最重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会自动生成一个默认的。
而C++11新增了两个默认成员函数: 移动构造函数和移动赋值运算符重载。
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
● 如果你没有自己实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,对于自定义类型成员,则需要看这个成员是否实现了移动构造?如果实现了就调用移动构造,没有实现就调用拷贝构造。
● 如果你没有自己实现移动赋值重载函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认的移动赋值。默认生成的移动赋值函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现了移动赋值?如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面的默认移动构造完全类似)
● 如果你提供了移动构造或者移动赋值,那编译器就不会再自动提供默认的拷贝构造和拷贝赋值。
🍉总结:你要自己实现移动构造或移动赋值,那其他的拷贝构造、拷贝赋值和析构你也要自己实现!如果想要编译器提供默认的移动构造和移动赋值,则拷贝构造、拷贝赋值和析构这三个你都不要实现。即要实现就都实现,不实现就都不实现(构造除外)。
类成员变量初始化
C++11允许在类定义时给成员变量初始缺省值,默认生成的构造函数会使用这些缺省值初始化,这个我们在类和对象章节就讲了,这里就不再细讲。
● 强制生成默认函数的关键字default
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如: 我们提供了拷贝构造,则就不会生成默认的移动构造了,那么我们可以使用default关键字显式指定移动构造生成。
class Person
{
public:
//构造
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{ }
//拷贝构造
Person(const Person& p)
:_name(p._name)
, _age(p._age)
{ }
//强制编译器生成默认的移动构造
Person(Person&& p) = default;
private:
MyStr::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}
● 禁止生成默认函数的关键字delete
如果想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只是声明了补丁而已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明后加上delete即可,该语法指示编译器不生成对应函数的默认版本,称delete修饰的函数为删除函数。
class Person
{
public:
//构造
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{ }
//禁止编译器生成默认的拷贝构造
Person(const Person& p) = delete;
private:
MyStr::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
}
继承和多态中的final与override关键字
这个我们在继承和多态章节已经进行了详细的讲解,这里就不再细讲,需要的话去看继承和多态的文章。
二、可变参数模版
C++11的新特性可变参数模板能够让你创建可以接受可变参数的函数模板和类模板;可变参数意思是:允许模板接受任意数量和类型的参数。相比C++98/03,类模版和函数模版中只能含有固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。现阶段呢,我们掌握一些基础的可变参数模板特性就够我们用了,所以这里就点到为止,以后大家如果有需要,可以再深入学习。
下面就是一个基本可变参数的函数模板:
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args... args,这个参数包中可以包含0到任意个模板参数
template <class ...Args>
void ShowList(Args... args)
{ }
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用 args[i] 这样的方式获取可变参数,所以我们得用一些奇招来一一获取参数包的值。
想象理解:上面的模版参数包Args就代表类模板中有T1、T2…这些类型,而形参参数包args就代表着形参变量a、b…,他们分别是T1、T2…的类型。注意: 可变参数的书写格式要跟上面一样是加…的形式,至于Args或args可以用其他的名称代替。
可变模版参数支持用sizeof…去计算参数包args里有几个参数:
template <class ...Args>
void ShowList(Args... args)
{
cout << sizeof...(args) << endl; //注意这里的sizeof后要加...
}
int main()
{
ShowList(1);
ShowList(1, 2.2);
ShowList(1, 2.2, 'x');
ShowList(1, 2.2, 'x', true); //自动识别你传的参数个数及类型
return 0;
}

● 递归函数方式展开参数包
如果我们想解析args这个形参参数包的话,要怎么做呢?这个得用编译时的递归推演才能解析:
//递归终止函数
void _ShowList()
{
cout << endl;
}
//编译时的递归推演
//第一个模版参数依次解析获取参数值
template<class T, class ...Args>
void _ShowList(const T& val, Args... args)
{
cout << val << " ";
_ShowList(args...);
}
template <class... Args>
void ShowList(Args... args)
{
_ShowList(args...);
}
int main()
{
ShowList(1);
ShowList(1, 2.2);
ShowList(1, 2.2, 'x');
return 0;
}


注意:参数包里可以有0个参数,即参数包里没有参数。
● 逗号表达式展开参数包
这种展开参数包的方式,不需要通过递归终止函数,而是直接在expand函数体中展开,PrintArg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。
expand函数中的逗号表达式: (printarg(args), 0),也是按照这个执行顺序,先执行printarg(args),再返回逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组,{(printarg(args), 0)…}将会逐项展开成{(printarg(arg1),0), (printarg(arg2), 0), (printarg(arg3), 0), etc…},最终会创建一个元素值都为0的数组int arr[sizeof…(args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程中展开参数包。
🥑ps:PrintArg(args)…这种表示将参数包args中的每个参数依次展开,并分别作为PrintArg函数的独立参数调用。
例如,若args中包含(1, “hello”, 3.14),则展开为:
PrintArg(1), PrintArg(“hello”), PrintArg(3.14)
template <class T>
void PrintArg(T t)
{
cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { (PrintArg(args), 0)... };
cout << endl;
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}

其实上面不用逗号表达式也可以展开参数包,只是因为逗号表达式需要一个返回值,就写成了(PrintArg(args), 0)…的形式。其实写成PrintArg(args)…的形式,而构造数组元素的0让PrintArg函数返回即可:
template <class T>
int PrintArg(T t)
{
cout << t << " ";
return 0;
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { PrintArg(args)... };
cout << endl;
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
● STL容器中的empalce相关接口函数
其实很多容器中都有emplace系列的插入接口。比如list容器的emplace_back接口:
它是一个尾插函数,那它与push_back函数有什么区别吗?我们来看一下两者插入左值和右值的情况:(注: 下面的list是标准库里的)
可以看到插入s1(左值)没有区别,都是去调用MyStr::string的拷贝构造;而插入"hello"(右值)时,push_back会调用构造和拷贝构造,但emplace_back只会调构造。这是为什么呢?我们再试着往list容器里插入其他类型的元素看看,比如插入pair<MyStr::string, int>类型:
也就是说emplace_back在尾插元素时,通过不断往下传递参数包,编译器在编译时最终会展开这个参数包,自动推导参数包里的参数类型及个数(从左到右),去匹配_data类型的构造函数:
//list容器的节点的构造函数
template<class ...Args> //可变参数模版
list_node(Args&&... args) //万能引用
:_data(args...)
, _next(nullptr)
, _prev(nullptr)
{ }
typedef list_node<T> Node;
template<class ...Args> //可变参数模版
void emplace_back(Args&&... args) //万能引用
{
Node* newnode = new Node(args...);
//链接节点到链表中…
}
所以如果list中存的是pair<MyStr::string,int>类型的元素时,上面节点的构造函数中的_data(args…)就是在构造pair这个类型;参数包args…在编译阶段会展开为MyStr::string, int,就会去匹配调用pair的构造函数:
所以emplace系列的插入接口只是略微比push系列的好一点,并没有很大的提升;因为push系列的插入接口对于右值来说是移动构造,成本也是很低的。
🍓总结🍓:
三、lambda表达式
3.1 C++98中的一个例子
在C++98中,如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法。
#include<iostream>
#include <algorithm>
using namespace std;
int main()
{
int arr[] = { 4,1,8,5,3,7,0,9,2,6 };
// 默认按照小于比较,排出来结果是升序
std::sort(arr, arr + sizeof(arr) / sizeof(arr[0]));
for (auto i : arr)
{
cout << i << " ";
}
cout << endl;
// 如果需要降序,需要改变元素的比较规则
std::sort(arr, arr + sizeof(arr) / sizeof(arr[0]), greater<int>());
for (auto i : arr)
{
cout << i << " ";
}
cout << endl;
return 0;
}

如果待排序元素为自定义类型,需要用户定义排序时的比较规则:
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 } };
sort(v.begin(), v.end(), ComparePriceLess());
for (auto e : v)
{
cout << e._price << " ";
}
cout << endl;
sort(v.begin(), v.end(), ComparePriceGreater());
for (auto e : v)
{
cout << e._price << " ";
}
cout << endl;
return 0;
}

随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类(仿函数),特别是相同类的命名这些都给编程者带来了极大的不便。因此,在C++11语法中出现了lambda表达式。
用lambda表达式解决上面的问题:
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2)->bool { return g1._price < g2._price; });
for (auto e : v)
{
cout << e._price << " ";
}
cout << endl;
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2)->bool { return g1._price > g2._price; });
for (auto e : v)
{
cout << e._price << " ";
}
return 0;
}

上述代码就是使用C++11中的lambda表达式来解决,可以看出lambda表达式实际是一个匿名函数,更准确的来说lambda表达式应该是一个匿名函数对象(仿函数)。
3.2 lambda表达式语法
○ lambda表达式的各部分说明
lambda表达式的书写格式:
[capture-list](parameters) mutable ->return-type {statement}
(1) lambda表达式各部分说明
[capture-list]:捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[ ]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
(parameters):参数列表。与普通函数的参数列表一致,如果不需要传递参数,则可以连同()一起省略。
mutable:默认情况下,lambda函数总是一个const函数(形象的说法),mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
->returntype:返回值类型。用追踪返回类型的形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确的情况下也可省略,由编译器对返回类型进行推导。
{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
🍇注意🍇:
在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{};该lambda函数不会做任何事情。
int main()
{
auto f1 = [](int x)->int {cout << x << endl; return 0; };
f1(1);
cout << typeid(f1).name() << endl;
//省略返回值,让编译器自己推导
auto f2 = [](int x)
{
cout << x << endl;
return 0;
};
f2(2);
cout << typeid(f2).name() << endl;
return 0;
}

通过上述例子可以看出,lambda表达式实际上可以理解为无名函数对象,该对象无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量。通过typeid(变量).name()可以看到上面两个lambda表达式的本质就是两个 (匿名)类,类名就是上面的一长串。既然f1和f2能像函数一样使用,说明类里面重载了operator()。
<lambda_uuid>:通常指通过Lambda表达式生成的唯一标识符(UUID)。UUID(通用唯一识别码)是用于生成全局唯一标识符的标准,通常基于时间、硬件地址或随机数生成。也就是不同的类要有不同的类名。
有了lambda表达式以后,就不用再自己写仿函数了!
○ 捕获列表说明
捕捉列表描述了上下文中哪些数据可以被lambda使用,以及使用的方式是传值还是传引用。
▲ [var]: 表示值传递方式捕捉变量var
▲ [=]: 表示值传递方式捕获所有父作用域中的变量(包括this)
▲ [&var]: 表示引用传递捕捉变量var
▲ [&]: 表示引用传递捕捉所有父作用域中的变量(包括this)
▲ [this]: 表示值传递方式捕捉当前的this指针
🔥注意🔥:
a.父作用域指包含lambda函数的语句块。
父作用域:是指代码中包裹当前作用域的外层范围,通常由花括号{}或其他语法结构(如函数、类、命名空间)定义。
语句块:是由一对花括号{}包裹的零个或多个语句组成的代码单元,用于限定作用域、组合逻辑或控制执行流程。
b.语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
比如:
- [=, &a, &b]: 以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量
- [&, a, this]: 值传递方式捕捉变量a和this,引用方式捕捉其他变量
c.捕捉列表不允许变量重复传递,否则就会导致编译错误。
比如:
[=, a]: =已经表示以值传递方式捕捉所有变量,则捕捉a重复。
d.在块作用域以外的lambda函数捕捉列表必须为空。
e.在块作用域中的lambda函数仅能捕捉父作用域中的局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
f.lambda表达式之间不能相互赋值,即使看起来类型相同。
比如现在想交换两个变量的值,用lambda表达式的方法如下:
int main()
{
int x = 1, y = 2;
cout << "x的地址:" << &x << " " << "y的地址:" << &y << endl;
cout << x << " " << y << endl;
//交换两个变量的值
//方法1:参数列表传引用
auto func1 = [](int& r1, int& r2)
{
int tmp = r1;
r1 = r2;
r2 = tmp;
};
func1(x, y);
cout << x << " " << y << endl;
//以值传递的方式捕获列表,这种不能交换两个变量的值,因为变量是值传递的形式
auto func = [x,y]() mutable
{
cout << "x的地址:" << &x << " " << "y的地址:" << &y << endl;
int tmp = x;
x = y;
y = tmp;
};
func(); //不用传参直接调用
cout << x << " " << y << endl;
//方法2:以引用传递的方式捕获列表
auto func2 = [&x, &y]() //[]中的&是引用,不是取地址
{
int tmp = x;
x = y;
y = tmp;
};
func2(); //不用传参直接调用
cout << x << " " << y << endl;
return 0;
}

注意:当[ ]中是值传递时,要在后面加上mutable才能修改捕获的实参,因为值传递的形式默认变量是用const修饰的,加mutable修饰,就是去掉const属性。当[ ]中是以值传递时,那其中的实参跟父作用域中的是不同的变量。当[ ]中是传引用时,即[&val]的形式,后面的mutable可以不加,因为传引用就是想修改捕获的变量,此时捕获的实参跟父作用域中的是同一个变量。
还有一个就是[捕获列表]中可以捕捉this指针,这是什么意思?看下面:
struct AA
{
public:
void func() //func函数的形数中隐含着this指针
{
auto f1 = [=]() //捕捉所有func函数作用域中的值,包括this指针
{
cout << this->a1 << " " << a2 << endl;
};
f1();
auto f2 = [this]() //值传递的方式捕捉当前的this指针
{
cout << a1 << " " << a2 << endl;
};
f2();
}
private:
int a1 = 1;
int a2 = 2;
};
当然lambda表达式还有一些特殊的用法,比如:
int main()
{
auto f1 = [] {cout << "hello world" << endl; };
//允许使用一个lambda表达式拷贝构造一个新的副本
auto f2(f1);
f2();
//可以将lambda表达式赋值给相同类型的函数指针
void (*PF)() = f2;
PF();
return 0;
}

○ 函数对象与lambda表达式
函数对象,又称为仿函数。即: 可以像函数一样使用的对象,就是在类中重载了operator()运算符的类对象。
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);
r1(10000, 2);
//lamber表达式
auto r2 = [=](double money, int year)->double {return money * rate * year;};
r2(10000, 2);
return 0;
}
从使用方式上来看,函数对象与lambda表达式完全一样。函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可以直接将该变量捕获到。

实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即: 如果定义了一个lambda表达式,编译器会自动生成一个类,并在该类中重载operator()。
四、包装器
4.1 function包装器
function包装器也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。那么我们来看看,我们为什么需要function呢?
ret = func(x);
// 那上面的func可能是什么呢?func可能是函数名?函数指针?函数对象(仿函数)?也有可能
// 是lambda表达式?所以这些都是可调用的类型!如此丰富的类型,可能会导致模板的效率低下!
// 为什么呢?我们继续往下看
//函数模版
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);
}
//函数名f是一个函数指针
double f(double i)
{
return i / 2;
}
//Functor是一个函数对象
struct Functor
{
double operator()(double d)
{
return d / 3;
}
};
int main()
{
//函数名
cout << useF(f, 11.11) << endl << endl;
//函数对象
cout << useF(Functor(), 11.11) << endl << endl;
//lambda表达式
cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
return 0;
}

通过上面的程序验证,我们会发现useF函数模板实例化出了三份。这样效率比较低下。
通过包装器就可以很好的解决上面的问题:
//std::function在头文件functional中
//类模板原型如下
template<class T> function; //undefined
template<class Ret, class ...Args>
class function<Ret(Args...)>;
模板参数说明:
Ret: 被调用函数的返回值类型
Args…:被调用函数的形参类型
包装器的使用方法如下:
int f(int a, int b)
{
return a + b;
}
struct Functor
{
int operator() (int a, int b)
{
return a + b;
}
};
int main()
{
//函数名(函数指针)
function<int(int, int)> func1 = f;
cout << func1(1, 2) << endl;
//函数对象
function<int(int, int)> func2 = Functor();
cout << func2(1, 2) << endl;
//lamber表达式
function<int(int, int)> func3 = [](const int a, const int b){return a + b; };
cout << func3(1, 2) << endl;
return 0;
}

(1) 包装器就是在统一可调用对象的接口:
🍐function能够存储多种类型的可调用对象,并通过类型擦除技术将其统一封装为相同的接口。这种设计避免了为每种可调用对象编写重复代码,简化了回调机制和泛型编程的实现。
可调用对象就是指:函数指针、仿函数及lambda表达式。
(2) 优化模板实例化效率
🍊在模板编程中,直接使用不同类型的可调用对象会导致编译器生成大量重复代码。function通过统一封装减少了模板实例化的次数,提升了编译效率和代码复用性。
有了包装器,就可以解决上面模板效率低下,实例化多份的问题:
int main()
{
//函数名
function<double(double)> func1 = f;
cout << typeid(func1).name() << endl;
cout << useF(func1, 11.11) << endl << endl;
//函数对象
function<double(double)> func2 = Functor();
cout << typeid(func2).name() << endl;
cout << useF(func2, 11.11) << endl << endl;
//lambda表达式
function<double(double)> func3 = [](double d)->double { return d / 4; };
cout << typeid(func3).name() << endl;
cout << useF(func3, 11.11) << endl;
return 0;
}

即: 如果你用function来包装这三个可调用对象,且这三个可调用对象的函数参数类型、个数及返回值类型相同,则它们会被转换成一个相同的function类型。第一次你将这个function类型的对象func1传给函数模版以后,实例化出了一个useF函数;跟后两次你传func2和func3调用的useF函数其实是同一个函数。
🍎注意:你包装的这些对象的函数参数类型、个数及返回值类型要和function里的 Ret, Args… 相匹配,才能包装。
所以有了function包装器,就可以将函数指针、仿函数及lambda表达式包装成一个统一的类型。这样在实例化模版时,就不用因为这三个对象的类型不同而去实例化三份:
//swap函数名是一个函数指针
void swap_func (int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
//Swap是一个函数对象
struct Swap
{
void operator()(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
};
int main()
{
int x = 1, y = 0;
cout << x << " " << y << endl;
//lambda表达式
auto swaplambda = [](int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
};
//function是一个包装器
map<string, function<void(int&, int&)>> cmpOP = {
{"函数指针",swap_func},
{"仿函数",Swap()},
{"lambda",swaplambda}
};
cmpOP["函数指针"](x, y);
cout << x << " " << y << endl;
cmpOP["仿函数"](x, y);
cout << x << " " << y << endl;
cmpOP["lambda"](x, y);
cout << x << " " << y << endl;
return 0;
}

包装器的其他一些使用场景:ReversePolish
//逆波兰表达式使用包装器解决
class Solution
{
public:
int evalRPN(vector<string>& tokens)
{
stack<int> st;
map<string, function<int(int, int)>> opFuncMap = {
{ "+", [](int i, int j){return i + j; } },
{ "-", [](int i, int j){return i - j; } },
{ "*", [](int i, int j){return i * j; } },
{ "/", [](int i, int j){return i / j; } }
};
for(auto& str:tokens)
{
if(opFuncMap.count(str))
{
int right = st.top();
st.pop();
int left = st.top();
st.pop();
st.push(opFuncMap[str](left,right));
}
else
{
st.push(stoi(str));
}
}
return st.top();
}
};
注意:对类的成员函数用function包装器比较特殊,得用: &类名::成员函数 才可以。
class Plus
{
public:
//静态成员函数
static int plusi(int a, int b)
{
return a + b;
}
//普通成员函数
double plusd(double a, double b)
{
return a + b;
}
};
int main()
{
//类的静态成员函数(不含this指针)
function<int(int, int)> func4 = &Plus::plusi;
cout << func4(1, 2) << endl;
//注意类的普通成员函数中隐含着this指针
function<double(Plus*, double, double)> func5 = &Plus::plusd;
Plus ps;
cout << func5(&ps, 1.1, 2.2) << endl;
function<double(Plus, double, double)> func6 = &Plus::plusd;
cout << func6(Plus(), 1.1, 2.2) << endl;
return 0;
}

可以发现对于类的普通成员函数,每次写function接收时,第一个参数总是要传这个类的对象或者对象的指针(this),很麻烦!有没有什么方法能把这个参数默认固定死,我们不用去传递,而只去传我们想传递的参数呢?这就要用到bind这个函数了。
4.2 bind
🍏bind函数也定义在functional的头文件中,它是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来 “适应” 原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没什么意义)参数的新函数。同时,使用bind函数还可以实现参数顺序调整等操作。
//原型如下:
template <class Fn, class... Args> //可变参数模版
bind(Fn&& fn, Args&&... args); //unspecified 万能引用
//with return type(2)
template <class Ret, class Fn, class... Args> //可变参数模版
bind(Fn&& fn, Args&&... args); //unspecified 万能引用
可以将bind函数看作是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来 “适应“ 原对象的参数列表。
调用bind的一般形式为:
🔥auto newCallable = bind(callable, arg_list);
其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数。
arg_list中的参数可能包含形如 _n 的名字,其中n是一个整数,这些参数是 “占位符”,表示newCallable的参数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对象中参数的位置: _1 为newCallable的第一个参数,_2 为第二个参数,以此类推。
举个栗子:
int Sub(int a, int b)
{
return a - b;
}
int main()
{
function<int(int, int)> f1 = Sub;
cout << f1(10, 5) << endl;
//调整参数顺序
function<int(int, int)> f2 = bind(Sub,placeholders::_2, placeholders::_1);
cout << f2(10, 5) << endl;
//调整参数个数,有些参数可以在bind时直接绑定死
function<int(int)> f3 = bind(Sub, 20, placeholders::_1);
cout << f3(10) << endl;
return 0;
}



那现在就可以解决上面:类的成员函数每次调用都要传这个类的对象或对象指针的问题啦:
int main()
{
//类的成员函数
function<int(int, int)> func4 = &Plus::plusi;
cout << func4(1, 2) << endl;
//绑定成员函数
Plus ps;
//将类对象的指针绑定死
function<double(double, double)> func5 = bind(&Plus::plusd, &ps, placeholders::_1, placeholders::_2);
cout << func5(1.1, 2.2) << endl;
//将类对象绑定死
function<double(double, double)> func6 = bind(&Plus::plusd, Plus(), placeholders::_1, placeholders::_2);
cout << func6(1.1, 2.2) << endl;
return 0;
}

🥑总结🥑:
function的底层是一个类模版,里面重写了operator()接口。通过调用operator()来统一处理不同类型的可调用对象。其核心机制是通过类型擦除(type erasure)技术,将各种可调用对象(如函数指针、函数对象及lambda表达式等)的调用接口统一为operator()形式。

更多推荐
所有评论(0)