文章目录
一、可变模板参数
1、概念
C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比 C++98/03,类模版和函数模版中只能含固定数量的模版参数。
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
2、可变模板参数展开
(1)递归函数方式展开参数包
通过递归的方式,将参数包中的参数一个一个取出。
//递归结束条件
template <class T>
void func10(T t)
{
cout << t << " ";
}
//递归将参数包展开
//T 取上次调用的参数包中的第一个,Args取剩下的参数
template <class T , class ...Args>
void func10(T t,Args... args)
{
cout << t << " ";
func10(args...);
}
//接受参数
template <class ...Args>
void test15(Args... args)
{
func10(args...);
}
(2)逗号表达式展开参数包
这种展开参数包的方式,不需要通过递归终止函数,是直接在func12函数体中展开的, func11不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。 func12函数中的逗号表达式:(func11(args), 0),也是按照这个执行顺序,先执行 func11(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(func11(args), 0)…}将会展开成((func11(args1),0), (func11(args2),0), func11(args3),0), etc… ),最终会创建一个元素值都为0的数组int> arr[sizeof…(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部func11(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包
//获取一个参数
template <class T>
void func11(T t)
{
cout << t << " ";
}
template <class ...Args>
void func12(Args... args)
{
//变长数组
int arr[] = { (func11(args),0)... };
}
void test16()
{
func12(10, 11.1, 12, 13.3);
}
3、使用场景
(1)标准库中的有些容器实现接口时使用
如:
(2)模拟list实现一个emplace_back
// List的结点类
template<class T>
struct ListNode
{
ListNode<T>* _pPre; //后继指针
ListNode<T>* _pNext; //前驱指针
T _val; //数据
//构造结点
ListNode(const T& val = T()) :_val(val), _pPre(nullptr), _pNext(nullptr)
{}
//接受参数包的节点构造函数
template <class... Args>
ListNode(Args &&... args) :_val(forward<Args>(args)...), _pPre(nullptr), _pNext(nullptr)
{}
};
template <class... Args>
void emplace_back(Args&&... args)
{
//使用参数包构造节点
//注意:使用forward时格式:forward<Args>(args)...
PNode tmp = new Node(forward<Args>(args)...);
//将链表(带头双向链表)连接到尾
//_pHead头节点
PNode priv = _pHead->_pPre;
tmp->_pPre = priv;
tmp->_pNext = _pHead;
priv->_pNext = tmp;
}
(3)push_back
与emplace_back
比较
场景一
ls.push_back({ 1,1 });
ls.emplace_back(1, 1);
push_back
会先进行一次构造,再进行移动构造节点中储存数据成员变量。
emplace_back
传递参数包,最后用参数包直接构造节点中储存数据成员变量。
场景二
pair<int, int> p(1, 1);
ls.push_back(p);
ls.emplace_back(p);
push_back
会先进行一次拷贝构造,再进行移动构造节点中储存数据成员变量。
emplace_back
传递参数包,最后用参数包中的pair<int,int>
对象进行拷贝构造节点中储存数据成员变量。
二、 lambda表达式
1、引入
在使用sort
排序pair<int,int>
时,需要我们传递函数对象进行比较,如:
class cmp
{
public:
bool operator()(const pair<int, int>& p1, const pair<int, int>& p2)
{
return p1.first > p2.first;
}
};
vector<pair<int, int>> arr = { make_pair(1,2), make_pair(3,2), make_pair(0,2) };
sort(arr.begin(), arr.end(),cmp());
如果像这样每次都需要写一个类来传递函数对象的话就太麻烦了,于是在c++11就引入了 lambda
表达式
2、 lambda表达式语法
lambda
表达式书写格式:
[capture-list] (parameters) mutable -> return-type { statement
}
[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来 判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda 函数使用。
(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以 连同()一起省略。
mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量
性。使用该修饰符时,参数列表不可省略(即使参数为空)。
->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回 值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获 到的变量。
注意:
在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为
空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。
3、演示
void test18()
{
//使用`sort`排序`pair<int,int>`
vector<pair<int, int>> arr = { make_pair(1,2), make_pair(3,2), make_pair(0,2) };
sort(arr.begin(), arr.end(), [](const pair<int, int>& p1, const pair<int, int>& p2) {return p1.first < p2.first; });
for (auto& e : arr)
cout << e.first << " " << e.second << endl;;
{
int x = 10;
auto func1 = [x](const int& a,const int& b)mutable->int {x = 20; return a * x + b; };
cout <<"func1: " << func1(x, x) << endl;
}
{
double p = 3.14;
auto func2 = [=](const double& r) { return p * r * r; };
cout << "func2: "<<func2(2) << endl;
}
}
4、函数对象
函数对象,又称为仿函数,即可以像函数一样使用的对象,就是在类中重载了operator()
运算符的类对象。
class area
{
public:
area(double pi) :_pi(pi) {};
double operator()(const double& r)
{
return r * r * _pi;
}
private:
double _pi;
};
void test19()
{
const double pi = 3.14;
area a(pi);
auto b = [pi](const double& r) {return r * r * pi; };
cout << "a: " << a(2) << endl;
cout << "b: " << b(2) << endl;
}
从使用方式上来看,函数对象与lambda
表达式完全一样。
函数对象将rate
作为其成员变量,在定义对象时给出初始值即可,lambda
表达式通过捕获列表可以直接将该变量捕获到。
实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda
表达式,编译器会自动生成一个类,在该类中重载了operator()
。
三、 包装器
1、function包装器
(1)介绍
function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。
std::function在头文件< functional >中。
(2)使用function
包装器
//函数对象
class area
{
public:
double operator()(const double& r)
{
return r * r * _pi;
}
private:
double _pi = 3.14;
};
//普通函数
double f(const double& r)
{
return r * r * 3.14;
}
template <class R,class F>
double func13(R r, F f)
{
return f(r);
}
#include<functional>
void test20()
{
//函数对象
function<double(const double&)> func1 = area();
//普通函数
function<double(const double&)> func2 = f;
//lambda表达式
function<double(const double&)> func3 = [](const double& r) {return r * r * 3.14; };
//调用
cout<<func13(1,func1)<<endl;
cout<<func13(2,func2)<<endl;
cout << func13(3, func3) << endl;
}
在上面的例子中使用包装器与不使用包装器的区别:
不使用包装器时,函数对象/普通函数/lambda
表达式代表的是不同类型,这样func13
这个函数就会生成三份实例。
使用包装器后,函数对象/普通函数/lambda
表达式通过包装器包装后形成的只有一个类型function<double(const double&)>
,所以func13
只会生成一份实例。
2、bind包装器
(1)介绍
std::bind函数定义在头文件中
#include < functional>
,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callableobject),生成一个新的可调用对象来“适应”原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作。
(2)使用bind包装器
placeholders : 绑定参数占位符,配合bind包装器使用。
int func14(int a, int b, int c)
{
return (a - b) * c;
}
void test21()
{
//调整顺序
auto f1 = std::bind(func14, placeholders::_3, placeholders::_2, placeholders::_1);
cout << f1(2, 1, 4) << endl;
//绑定
auto f2 = std::bind(func14,placeholders::_2,placeholders::_1,10);
cout << f2(5,8) << endl;
}
(3)bind包装器和function包装器配合使用
void test22()
{
//调整顺序
function<int(int,int,int)> f1 = std::bind(func14, placeholders::_3, placeholders::_2, placeholders::_1);
cout << f1(2, 1, 4) << endl;
//绑定
function<int(int, int)> f2 = std::bind(func14, placeholders::_2, placeholders::_1, 10);
cout << f2(5, 8) << endl;
}