目录
一. 可变参数模板
1.1 什么是可变模板参数
1.2 参数包具体值的获取
1.3 emplace/emplace_back接口函数
二. lambda表达式
2.1 lambda表达式的概念和定义方法
2.2 捕捉列表说明
2.3 lambda表达式的底层实现原理
三. 包装器
3.1 function包装
3.2 bind绑定
3.2.1 bind改变参数顺序
3.2.2 bind调整参数个数
四. 总结
一. 可变参数模板
1.1 什么是可变模板参数
在C++98标准中,模板只能包含固定个数的参数,但到了C++11,就允许了模板参数的个数可变,可变模板参数主要用于函数模板,其定义方式为:
template<class ...Args>
void Func(Args... args) { ... }
其中,...Args表示可变参数模板类型,args可以接收0~N个不同类型的参数,我们称args为参数包。通过 std::cout << sizeof...(args) << std::endl 我们可以获取可变参数的个数。
template<class ...Args>
void Func(Args... args)
{
std::cout << sizeof...(args) << std::endl; //获取可变参数列表的参数个数
}
int main()
{
Func(1, 2, 3, 4); // 4
Func(1, 2, 3); // 3
Func(1, 2); // 2
Func(1); // 1
return 0;
}
1.2 参数包具体值的获取
注意,C++语法不支持通过args[i]来获取参数包的值。因此,无法通过下标来直接获取值。这就要求我们采取巧妙的办法,逐个遍历参数包args的每个值。主要的方法有两种:
- 递归函数展开参数包。
- 逗号表达式展开函数包。
如果采用递归函数展开函数包,就要去控制递归终止的条件,一般采用重载参数个数为0的函数,作为控制递归终止的函数。演示代码1.1以PrintList函数为例,不将第一个参数val归入可变参数包args,再使用完val时,以args...作为形参,传给PrintList函数实现递归调用,这是参数包args的第一个参数就充当递归调用函数的val参数。如果args中参数个数为0,就走PrintList的重载形式,函数递归调用终止。
代码1.1:递归方法展开参数包
void PrintList() //递归终止控制函数
{
std::cout << std::endl;
}
template<class T, class ...Args>
void PrintList(const T& val, Args... args)
{
std::cout << "<ListVal, 参数包中参数个数> : " << "<" << val << "," << sizeof...(args) << ">" << std::endl;
PrintList(args...);
}
int main()
{
std::string s = "zhang";
PrintList(30, 12.15, s, 'b', 100);
return 0;
}
如果采用逗号表达式来处理参数包,就需要定义一个int型数组(int a[]),这个int型数组要省略元素个数的声明,因为我们无法知道参数包中参数的具体个数。如代码1.2所示,定义了数组int a[] = { (_printList(args), 0)... },其中_printList(args)会对args的每个元素进行处理,依次将参数包中的每个数据作为参数带入到_printList函数中处理。
(_printList(args), 0)... 相当于依次执行:(_printList(args1), 0)、(_printList(args2), 0)、... 、(_printList(argsn), 0),其中argsi表示参数包的第i个参数。
代码1.2:采用逗号表达式展开参数包
template<class T>
void _PrintList(const T& val)
{
std::cout << val << std::endl;
}
template<class ...Args>
void PrintList(Args... args)
{
int a[] = { (_PrintList(args), 0)... };
}
其实,我们也并非一定要采取逗号表达式展开可变参数列表,也可以直接利用数组展开,具体做法为:让_PrintList函数返回int类型的数据,定义int a[] = { _PrintList(args)... }即可。
代码1.3:直接采用数组展开参数包
template<class T>
int _PrintList(const T& val)
{
std::cout << val << std::endl;
return 0;
}
template<class ...Args>
void PrintList(Args... args)
{
int a[] = { _PrintList(args)... };
}
1.3 emplace/emplace_back接口函数
由于C++11支持了可变模板参数,为此,在STL容器有关插入数据的接口函数中,新增了emplace和emplace_back接口,emplace系列接口函数支持传递可变个数的参数,其在vector容器中的声明见图1.2。
以vector的emplace_back接口为例,其与push_back的的对比如下:
- 如果vector中存储的是内置类型数据,那么emplace_back与push_back无论从用法还是底层实现层面,基本一致。
- 如果vector中存储自定义类型数据,如std::pair<int,int>,那么push_back必须以键值对作为参数,先构造函数std::pair作为函数参数,在调用std::pair的拷贝构造函数,而emplace_back可以直接传两个值作为键值对pair的first成员变量和second成员变量实现构造。即:v.emplace_back(1, 1)是可行的,但v.push_back(1, 1)是被禁止的,必须使用v.push_back(std::make_pair(1,1))显示构造键值对传参。
代码1.4:vector容器的emplace_back接口的使用
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{ }
private:
int _year;
int _month;
int _day;
};
int main()
{
std::vector<std::pair<int, int>> v1;
v1.emplace_back(1, 1);
v1.emplace_back(2, 2);
v1.push_back(std::make_pair(3, 3));
//v.push_back(1, 1); //禁止
std::vector<Date> v2;
v2.emplace_back(2023, 5, 29);
v2.emplace_back(2022, 6, 1);
v2.push_back(Date(2020, 5, 1));
return 0;
}
二. lambda表达式
2.1 lambda表达式的概念和定义方法
lambda表达式,其实本质上就是一个匿名函数对象,定义lambda表达式的语法为:
- [captrue-list](parameter)mutable->return_type { statement };
其中,每个部分的意义为:
- [captrue-list]:捕捉列表,将特定的变量拉取作为lambda表达式这个匿名函数对象的成员变量,使指定的变量可以在lambda表达式的函数体{ statement }中使用。
- paremeter:参数列表,与普通函数的参数列表一致。
- mutable:如果lambda表达式不声明mutable,那么就认为它是const属性的成员函数,如果[captrue-list]以值捕捉的方法捕捉特定变量,那么在lambda表达式中就不能对捕捉的变量进行修改。注意:若使用引用捕捉,即使不声明mutable,依旧可以修改引用对象的值。
- ->return_type:返回值类型,在大部分情况下省略,由编译器自动推导。
- { statement }:函数的具体实现代码。
代码2.1定义了两个用于加法计算lambda表达式对象add1和add2,用于实现加法计算函数,add2省略返回值类型。还定义了一个利息计算函数函数InterestIncome函数,在捕获列表中以值捕捉的方式拉取利率rate作为成员变量,在函数实现的过程中直接使用rate。
代码2.1:lambda表达式的定义
int main()
{
auto add1 = [](int x, int y)->int {return x + y; };
auto add2 = [](int x, int y) {return x + y; }; //两个进行加法计算的lambda表达式
int x = 10, y = 20;
int ret1 = add1(x, y);
int ret2 = add2(x, y);
std::cout << "ret1 = " << ret1 << std::endl;
std::cout << "ret2 = " << ret2 << std::endl;
double rate = 0.05;
int money = 10000, year = 3;
auto Interest = [rate](int money, int year) {return rate * year * money; }; //利息计算lambda表达式
double InterestIncome = Interest(money, year);
std::cout << "InterestIncome = " << Interest(money, year) << std::endl;
return 0;
}
2.2 捕捉列表说明
捕捉列表捕捉变量,有值捕捉和引用捕捉两种方式:
- [变量名]:值捕捉。
- [=]:采用值捕捉的方式捕捉父类作用域所有变量,包括this指针,父类作用域指lambda表达式所在的函数。
- [&变量名]:采用引用捕捉的方式捕捉指定变量。
- [&]:采用引用捕捉的方式捕捉父类作用域所有变量,包括this指针。
关于捕捉列表,有以下几点注意事项:
- 可以采用混合方式进行捕捉:[&, a] -- 采用值捕捉的方式捕捉变量a,采用引用捕捉的方式捕捉父类作用域的其它变量。 [=, &a] -- 采用引用捕捉的方式捕捉变量a,采用值捕捉的方式捕捉除a以外父类作用域的所有变量。
- 不能重复捕捉:[=, a] -- 以及以值捕捉的方式捕捉了父类作用域的全部变量,则后面的a属于重复捕捉,编译器会报错。
- 以值捕捉的方式捕捉变量时,如果不使用mutable进行修饰,那边捕捉的变量不能在lambda表达式内部被修改。
- 不能捕捉父类作用域以外的变量。
- 对于全局变量,即使不进行捕捉,也可以在lambda表达式中使用。
代码2.2:采用不同的方式捕捉变量
int g = 1;
int main()
{
int a, b, c, d;
a = b = c = d = 1;
//采用值捕捉的方式捕捉父类作用域全部变量
auto func1 = [=]()
{
std::cout << a << b << c << d << std::endl;
};
func1(); // 1111
//采用引用捕捉的方式捕捉父类作用域全部变量
auto func2 = [&]()
{
++a;
++b;
++c;
std::cout << a << b << c << d << std::endl;
};
func2(); // 2221
//混合捕捉:采用引用捕捉的方式捕捉d,值捕捉捕捉父类作用域其他变量
auto func3 = [=, &d]()
{
++d;
std::cout << a << b << c << d << std::endl;
};
func3(); // 2222
//混合捕捉:采用值捕捉的方法捕捉a,采用引用捕捉的方法捕捉父类作用域其他变量
auto func4 = [&, a]()
{
++b;
++c;
++d;
std::cout << a << b << c << d << std::endl;
};
func4(); // 2333
//直接在lambda中使用并更改全局变量g的值
auto func5 = []()
{
++g;
++g;
};
func5();
std::cout << "g = " << g << std::endl; // g = 3
return 0;
}
2.3 lambda表达式的底层实现原理
lambda表达式的本质为匿名函数对象,在底层的实现原理与仿函数类似。对于lambda表达式,编译器在底层实现时会将其这个匿名对象处理成名称为lambda_uuid的类对象,其中uuid为一种字符串生成算法,其多次调用产生相同字符串的概率微乎其微,可以认为每次都生成不同的字符串。lambda_uuid作为函数对象,调用其operator(),执行lambda表达式函数体内的代码。
三. 包装器
3.1 function包装
如代码3.1所示,我们定义了一个模板函数UseF,其中包含一个F的模板,F类型的变量f可以作为函数来使用。F可以接收的类型有:函数(函数指针)、仿函数对象、lambda表达式,但是,当F分别作为函数指针、函数对象和lambda表达式传给去实例化UseF时,会实例化出多个对象,即使三种类型的f参数执行完全一样的工作。
我们通过在UseF中定义static int类型的变量count并让其自加,依次用函数指针、仿函数对象、lambda表达式实例化UseF并运行代码,可以看出count的值并不会随着调用次数的增加而改变,因此实例化了多份UseF对象。
代码3.1:
template<class F, class T>
T UseF(const F& f, const T& x, const T& y)
{
static int count = 0;
++count;
std::cout << "count = " << count << std::endl;
return f(x, y);
}
int sub(int x, int y)
{
return x - y;
}
struct Sub
{
int operator()(int x, int y) const
{
return x - y;
}
};
int main()
{
int a = 10, b = 3;
int ret1 = UseF(sub, a, b); //函数指针调用
int ret2 = UseF(Sub(), a, b); //函数对象调用
int ret3 = UseF([](int x, int y) {return x - y; }, a, b); //lambda表达式调用
std::cout << "ret1 = " << ret1 << std::endl;
std::cout << "ret2 = " << ret2 << std::endl;
std::cout << "ret3 = " << ret3 << std::endl;
return 0;
}
由于F接收3种不同类型的参数会实例化出3份对象,这回造成编译时开销和空间浪费,那么有没有可能,让UseF实例化出一份对象,就能同时接收函数指针、函数对象和lambda表达式作为F的类型。答案是可以的。
通过使用std::function对函数进行包装,就可以将函数指针、函数对象和lambda表达式的实际类型归一化。
std::function进行包装的语法为:std::function<Ret(Agrs...)>,其中Ret为函数返回值的类型,Args为函数的形参列表。std::function<Ret(Args...)>也某种特殊的函数对象类型。
使用std::function要包头文件<functional>
代码3.2将函数指针、函数对象和lambda表达式用std::function进行包装,将包装后的std::function<int(int, int)>对象作为参数调用UseF模板函数,运行代码,可见每次调用count的值都会+1,证明只实例化了一份UseF函数。
代码3.2:std::function封装
template<class F, class T>
T UseF(const F& f, const T& x, const T& y)
{
static int count = 0;
++count;
std::cout << "count = " << count << std::endl;
return f(x, y);
}
int sub(int x, int y)
{
return x - y;
}
struct Sub
{
int operator()(int x, int y) const
{
return x - y;
}
};
int main()
{
int a = 10, b = 3;
std::function<int(int, int)> func1 = sub;
std::function<int(int, int)> func2 = Sub();
std::function<int(int, int)> func3 = [](int x, int y) {return x - y; };
int ret1 = UseF(func1, a, b);
int ret2 = UseF(func2, a, b);
int ret3 = UseF(func3, a, b);
return 0;
}
3.2 bind绑定
std::bind用于对函数参数的修饰,可用于改变参数顺序和改变参数个数。
3.2.1 bind改变参数顺序
假设定义了整数除法运算函数Div:
int Div(int x, int y)
{
return x / y;
}
在正常情况下,代码Div(a, b)执行的运算是a/b,那么有没有可能,通过DIv(a, b)来计算b/a呢,答案是可以的。通过std::bind绑定,即可改变参数的顺序。
std::bind绑定改变参数顺序,需要用到占位符,占位符被定义在命名空间std::placeholders里,其中_1为一个参数的位置,_2为第二个参数的位置,...
通过std::bind(函数名, 占位参数)即可调整参数顺序,可以采用std::function<Ret(args...)>类型的对象来接收经std::bind绑定生成的函数对象。
代码3.2:std::bind调整参数顺序
int main()
{
int a = 2, b = 10;
std::function<int(int, int)> DivOri = std::bind(Div, std::placeholders::_1, std::placeholders::_2); // DivOri(x,y)执行x/y
std::function<int(int, int)> DivSwap = std::bind(Div, std::placeholders::_2, std::placeholders::_1); // DivSwap(x,y)执行y/x
std::cout << DivOri(a, b) << std::endl; // 2/10 = 0
std::cout << DivSwap(a, b) << std::endl; // 10/2 = 5
return 0;
}
如果std::bind要绑定某个类的成员函数,则std::bind的尖括号<>第一个成员必须为&类域::成员函数名,第二个参数为类对象(充当this),从第三个参数开始才是参数占位符,即使不调整参数顺序,占位符也不能省略。
代码3.3:std::bind绑定成员函数
class Sub
{
public:
int sub(int x, int y)
{
return x - y;
}
};
int main()
{
std::function<int(int, int)> sub1 = std::bind(&Sub::sub, Sub(), std::placeholders::_1, std::placeholders::_2);
int ret = sub1(10, 4);
std::cout << "ret = " << ret << std::endl; // ret = 6
return 0;
}
3.2.2 bind调整参数个数
假设我们要实现这样一颗搜索树,它的节点的Key值对应+、-、*、/四种操作符,根据输入的key值,匹配Value,Value为函数指针、函数对象、lambda表达式的任意一种,用以完成对应的四则运算操作。
我们在设定map的类型时,键值对Value必须给死类型,也就是说,Value的类型不可变,那么即使传经function封装后的对象,也必须保证Ret和args完全一致。但是,某些四则运算函数可能是某个类的成员函数,那么,我们就要采用bind绑定,来调整参数个数。如代码3.4,sub和add为成员函数,都要经function包装处理,然后作为map的Value对map初始化。
bind调整参数个数的本质,就是将某个确定的参数写死,如:调用某个成员函数,显示给定第一个参数为匿名对象,这个匿名对象充当指针。
代码3.4:std::bind绑定改变参数个数
struct Add
{
int add(int x, int y)
{
return x + y;
}
};
struct Sub
{
int sub(int x, int y)
{
return x - y;
}
};
int Mul(int x, int y)
{
return x * y;
}
int Div(int x, int y)
{
return x / y;
}
int main()
{
std::function<int(int, int)> addFunc = std::bind(&Add::add, Add(), std::placeholders::_1, std::placeholders::_2);
std::function<int(int, int)> subFunc = std::bind(&Sub::sub, Sub(), std::placeholders::_1, std::placeholders::_2); //bind绑定改变参数个数
std::function<int(int, int)> mulFunc = Mul;
std::function<int(int, int)> divFunc = Div;
std::map<char, std::function<int(int, int)>> opMap =
{
{'+', addFunc}, {'-', subFunc},
{'*', Mul}, {'/', divFunc}
};
int a = 6, b = 3;
std::string op = "+-*/";
for (const auto& ch : op)
{
if (opMap.find(ch) != opMap.end())
{
std::cout << opMap[ch](a, b) << std::endl;
}
}
}
std::bind增加的参数,并不一定是类对象,也可能是某个内置类型的变量,或是某个字面常量,代码3.5中定义了函数Interests来计算利息,函数有三个参数:rate表示利率、money表示存钱数、year表示存储年份,先定义两个函数对象InterestsFunc1和InterestsFunc2,通过bind写死rate的值,调用两个函数对象来计算利息时只用给定money和year的值即可。
代码3.5:bind增加参数个数(增加的参数为内置类型变量/字面常量)
double Interest(double rate, double money, double year)
{
return rate * money * year;
}
int main()
{
double rate = 0.05;
double money = 1000, year = 3;
std::function<double(double, double)> InterestFunc1 = std::bind(Interest, rate, std::placeholders::_1, std::placeholders::_2);
std::function<double(double, double)> InterestFunc2 = std::bind(Interest, 0.05, std::placeholders::_1, std::placeholders::_2);
std::cout << InterestFunc1(money, year) << std::endl; // 150
std::cout << InterestFunc2(money, year) << std::endl; // 150
return 0;
}
四. 总结
- C++11支持了可变参数模板,语法为:template<class ...Args> func(Args... args) { ... },不可以通过args[i]获取参数包args中参数的值,可以通过递归调用或逗号表达式的方式,获取参数包中参数的值。
- lambda的本质为匿名函数表达式,可用于实现与函数指针和仿函数相同的功能,定义lambda的语法格式为:[captrue-list](parameters)mutable->return_type { statement },其中[capture-list]为捕捉列表,(parameters)为函数参数列表,mutable如果省略则默认lambda表达式为类的const成员函数,return_type为返回值类型,一般省略由编译器自动推导,{statement}为函数体。
- 捕捉列表[capture-list]捕捉变量的方式可分为值捕捉和引用捕捉,[&]和[=]可分别实现以引用捕捉和值捕捉的方式对父类作用域全部变量的捕捉。还可以存在混合捕捉如[&, a]/[=, a],不可以重复捕捉如[=, a]。
- lambda表达式的底层实现也是通过operator()成员函数实现的。
- 通过std::function可以实现对函数指针、函数对象和lambda表达式的封装,经std::function封装后,实现相同功能的函数指针、函数对象和lambda表达式的类型可以相同,统一为:std::function<Rets(args...)>,其中Rets为函数返回值类型,args...为函数参数列表。
- bind绑定可以实现对函数参数顺序和参数个数的调整。