Hello大家好!很高兴我们又见面啦!给生活添点passion,开始今天的编程之路!
我的博客:<但凡.
我的专栏:《编程之路》、《数据结构与算法之美》、《题海拾贝》、《C++修炼之路》
欢迎点赞,关注!
目录
1、可变参数模板
1.1、基本语法
1.2、包扩展
1.3、emplace系列接口
2、类的新功能
2.1、默认的移动构造和移动赋值
2.2、委托构造
2.3、default和delete
1、可变参数模板
可变参数模板(Variadic Templates)是C++11引入的一个重要特性,它允许模板接受任意数量和类型的参数。
1.1、基本语法
可变参数模板可以使函数模板或者类模板支持任意多个类型的变量。可变数目的参数被称为参数包。存在两种参数包,第一种是模板参数包,第二种是函数参数包。参数包可以接受0个参数。
template<class ...Args> void func(Args... args) {}
template<class ...Args> void func(Args&... args) {}
template<class ...Args> void func(Args&&... args) {}
比如,在上面三个函数模板中,Arges是模板参数包,arges是函数参数包。我们需要注意一下格式,注意一下三个点的位置。
对于上面的三个函数模板,第三个函数模板我们使用的是右值引用,这意味着对于参数包中的每个类型都是使用的万能引用。如果传左值,这个参数包中的类型被推导为左值引用,引用折叠后为左值引用。如果传右值的话,类型被推导为右值引用,折叠后为右值引用。
可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
我们可以使用sizeof...来计算参数包中参数的个数。
#include<iostream>
using namespace std;template<class ...Args>
void func(Args... args)
{cout << sizeof...(args) << endl;
}
int main()
{func();//0func(1);//1func(1, 2);//2func(1, 2,"sss");//3return 0;
}
这个计算也是编译时计算,因为本质上我们就是实例化出四个函数。其实参数包也是使模板进一步的泛型化。
本质上是替换了这四个函数:
void func()
{}
void func(int a)
{}
void func(int a,int b)
{}
void func(int a,int b,const char* str)
{}
1.2、包扩展
现在我们实现一个print,对于传进函数的每一个变量都打印一次。
注意参数包不能这样用,因为参数包不是一个容器:
template <class ...Args>
//void Print(Args... args)
//{
// // 可变参数模板编译时解析
// // 下⾯是运⾏获取和解析,所以不⽀持这样⽤
// cout << sizeof...(args) << endl;
// for (size_t i = 0; i < sizeof...(args); i++)
// {
// cout << args[i] << " ";
// }
// cout << endl;
//}
那么我们怎么实现print函数呢?我们可以使用包扩展来实现。
包扩展是在编译时进行的。包扩展本质上是递归调用,但是是在编译时递归。
void ShowList()
{// 编译器时递归的终⽌条件,参数包是0个时,直接匹配这个函数 cout << endl;
}
template <class T, class ...Args>
void ShowList(T x, Args... args)
{cout << x << " ";// args是N个参数的参数包 // 调⽤ShowList,参数包的第⼀个传给x,剩下N-1传给第⼆个参数包 ShowList(args...);
}
// 编译时递归推导解析参数
template <class ...Args>
void Print(Args... args)
{ShowList(args...);//注意传参的时候三个点又写到后面了
}
int main()
{Print();Print(1);Print(1, "xxxxx");Print(1, "xxxxx", 2.2);return 0;
}
我们把参数传给print,然后print在调用showlist,每次“剔除”一个变量。知道参数包中变量数为0,此时再去调用showlist直接调用最上面的showlist函数。
正因为是编译时调用,所以我们不能这样写:
template <class T, class ...Args>
void ShowList(T x, Args... args)
{if (sizeof...(args) == 0){return;}cout << x << " ";// args是N个参数的参数包 // 调⽤ShowList,参数包的第⼀个传给x,剩下N-1传给第⼆个参数包 ShowList(args...);
}
因为这个函数结束条件是运行时判断逻辑。
还有一种包扩展的方式:
template <class T>
const T& GetArg(const T& x)
{cout << x << " ";return x;
}
template <class ...Args>
void Arguments(Args... args)
{cout << endl;
}
template <class ...Args>
void Print(Args... args)
{// 注意GetArg必须返回或者到的对象,这样才能组成参数包给Arguments Arguments(GetArg(args)...);
}
我们把传入print的几个参数组成参数包传给GetArg。对于GetArg来说,我们相当于把参数包中的每个参数都传给GetArg,然后把GetArg的所有返回值在组成一个参数包,传给Arguments. 这种包扩展就是普通的编译推导,并不是递归。
需要注意的是上面这种的包扩展方式在vs上是倒序输出的,也就是说输出结果和我们的传参顺序恰好相反。造成这种结果的原因是C++标准没有规定函数参数的求值顺序,而大部分编译器默认是从右到左。
1.3、emplace系列接口
C++11之后所有的stl容器都新增了emplace接口,empalce系列的接口均为模板可变参数。接下来我们使用list来测试一下emplace系列的接口。
对于emplace_back和push_back来说,无论是传左值,还是传右值,效率都没有区别。唯一有区别的场景就是这种:
int main()
{list<string> li;li.push_back("sss");//构造加移动构造li.emplace_back("sss");//直接构造return 0;
}
对于push_back来说,因为push_back这个函数的形参就是string&&类型的,如果我们想让形参和实参匹配上,需要先对形参进行构造,构造一个临时对象,然后再移动构造给string对象进行push_back。
但是对于emplace_back来说,由于他是一个模板,他可以直接接受const char*类型的变量,然后再在插入之前进行一次构造,构造出string对象进行插入就可以了。
接下来我们看下面这个场景:
#include<iostream>
#include<list>
#include<string>
using namespace std;
int main()
{list<pair<int,double>> li;li.push_back({ 6,5.5 });//li.emplace_back({ 6,5.5 });li.emplace_back(6, 5.5);return 0;
}
对于pair类型,我们使用emplace接口时不能传花括号,也就是说不能传初始化列表。因为咱们的emplace_back是模板函数,所以在传参是他会去推导类型。由于initializer_list在推导时必须是initializer_list<T>,也就是说列表中的值类型必须是相同的。但是这里一个int,一个double不是同一类型参数,所以说会编译报错。
但是对于直接传构造底层对象的参数是没有问题的。这也是emplace系列接口的正确用法:在插入值时向emplace系列接口中直接传入构造底层存储对象类型所需的参数。这样相比普通插入效率更高。但如果容器存储的是一些基本类型(如int,double,char)时使用emplace_back或push_back效率上没有差异。
现在我们对之前模拟实现的list进行一下升级,写一下emplace系列接口:
......
list_node(T&& x):_next(nullptr), _prev(nullptr), _data(std::move(x))
{}
//如果上面两个构造函数都给了默认值=T(),那么当new node时,也就是不传参是无法确定匹配哪个构造函数。
//一般右值版本不给默认值。右值引用通常用于移动语义,而默认构造的临时对象(T())不适合被移动。
// 语义上矛盾:右值引用表示要"窃取"资源,但默认参数会创建一个新对象
template<class ...Args>
list_node(Args&&... args): _next(nullptr), _prev(nullptr), _data(std::forward<Args>(args)...)
{}
......
template<class ...Args>
void emplace_back(Args&&... args)
{emplace(end(), std::forward<Args>(args)...);
}
template<class ...Args>
void emplace(iterator pos, Args&&... args)
{Node* cur = pos._node;Node* prev = cur->_prev;Node* newnode = new Node(std::forward<Args>(args)...);// prev newnode curprev->_next = newnode;newnode->_prev = prev;newnode->_next = cur;cur->_prev = newnode;++_size;
}
注意的是我们的参数包构造不需要解析参数包,也就是说编译器不需要进行包扩展什么的,编译器不需要一个一个看都是什么类型,而是直接拿去和list底层存储类型的构造函数匹配。如果匹配对不上就报错。
另外就是每次传参数包我们都需要完美转发。让参数包中的参数都保持原有属性。
2、类的新功能
2.1、默认的移动构造和移动赋值
C++11新增了两个默认成员函数:移动构造函数和移动赋值运算符重载。
如果你没有自己生成移动构造函数,且没有实现析构函数,拷贝构造,拷贝赋值重载中的任意一个,那么编译器就会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
同理,如果你没有自己实现移动赋值重载函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会 执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。
如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
2.2、委托构造
class A
{
public:A(int a, int b):_a(a),_b(b){}A(int a, int b, char c):A(a, b){_c = c;}
private:int _a;int _b;char _c=0;
};
上面这个类就实现了委托构造,本质上也是一种复用。
2.3、default和delete
default可以强制让编译器生成默认函数:
class MyClass {
public:MyClass() = default; // 显式要求编译器生成默认构造函数MyClass(const MyClass&) = default; // 默认拷贝构造函数
};
delete就与default相反,不让编译器生成某个默认函数:
class NonCopyable {
public:NonCopyable(const NonCopyable&) = delete;NonCopyable& operator=(const NonCopyable&) = delete;
};
好了,今天的内容就分享到这,我们下期再见!