赞
踩
目录
在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;
- }
注意,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)... };
- }
由于C++11支持了可变模板参数,为此,在STL容器有关插入数据的接口函数中,新增了emplace和emplace_back接口,emplace系列接口函数支持传递可变个数的参数,其在vector容器中的声明见图1.2。
以vector的emplace_back接口为例,其与push_back的的对比如下:
代码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表达式,其实本质上就是一个匿名函数对象,定义lambda表达式的语法为:
其中,每个部分的意义为:
代码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:采用不同的方式捕捉变量
- 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;
- }
lambda表达式的本质为匿名函数对象,在底层的实现原理与仿函数类似。对于lambda表达式,编译器在底层实现时会将其这个匿名对象处理成名称为lambda_uuid的类对象,其中uuid为一种字符串生成算法,其多次调用产生相同字符串的概率微乎其微,可以认为每次都生成不同的字符串。lambda_uuid作为函数对象,调用其operator(),执行lambda表达式函数体内的代码。
如代码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;
- }
std::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;
- }
假设我们要实现这样一颗搜索树,它的节点的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;
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。