目录
8。移动赋值和移动构造(2)+default / delete关键词
本文提到的List和string的自我实现方法即代码以在C++11(2)中出现,详情请在主页查看C++11(2)
8。移动赋值和移动构造(2)+default / delete关键词
上一篇文章我们讲了如何使用和理解移动赋值和移动构造,由于这两个新增的和原本类里面的6大成员函数类似,所以会有我们不需要自己写的时候吗?
class Person
{
public:
Person(const char* name = "111111111111", int age = 0)
:_name(name)
, _age(age)
{}
//Person(Person&& p) = default;//自动生成构造函数,关键词default
//Person& operator=(Person && p) = default;
//Person(const Person& p) = default;
//Person& operator=(const Person& p) = default;
//自动生成拷贝构造和移动构造
//如果加入了析构函数就不会生成移动拷贝了
//~Person()
//{}
private:
bit::string _name;//person类有一个自定义类型的成员
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
Person s4;
s4 = std::move(s2);
return 0;
}
person类由于类里面有一个自定义类型的成员,所以有移动赋值和移动拷贝的必要
上面可以看出调用了string的移动拷贝,现在我们将析构函数写上,如下
发现只能调用深拷贝了,咦难道是写了析构函数的原因吗?这里可没有那么简单!!!
以上说明,自动生成移动构造和移动赋值函数的条件还是比较苛刻的,需要同时没有写深拷贝函数,析构函数,拷贝赋值重载(operator=)。
class Person
{
public:
Person(const char* name = "111111111111", int age = 0)
:_name(name)
, _age(age)
{}
// 只声明不实现,声明为私有
// C++98禁止初始化和拷贝的办法
//private:
// Person(const Person& p);
// Person& operator=(const Person & p);
Person(const Person& p) = delete;//C++11的办法,禁止生成左值的构造函数关键词delete,防止拷贝
Person& operator=(const Person& p) = delete;
private:
bit::string _name;
int _age;
};
int main()
{
//有关于传入person类型对象的左值或者右值的都被禁止了所以外面都无法进行拷贝和初始化操作的
string s4 = "fgtgtht";
Person s3("sdfvfdg");
Person s1;
Person s2;
//Person s2 = s1;//构造赋值重载都被delete禁止了
//Person s3 = std::move(s1);//构造赋值重载都被delete禁止了
Person s6;
//s4 = std::move(s2);//构造赋值重载都被delete禁止了
return 0;
}
9。可变参数模板
//可变模板参数,参数包
// 可变模版参数
// 参数类型可变
// 参数个数可变
// 打印参数包内容
//向一个函数传递参数包的本质是在减少拷贝构造,因为可以直接通过这个
//参数包进行直接构造,然后参数包也支持层层传递
//对于可变模板参数来说可以传递无数种类型的无数个参数,突出一个可变
//这个语法的使用有点颠覆人的认知, 所以这个语法不常用,会看就行
void Print()//当x没有了的时候就会自动匹配到这里,然后递归结束
{
cout << endl;
}
template<class T, class... Args>
void Print(T&& x, Args... args)//对于模板来说,...是在可变参数类型的后面级参数的前面
{
cout << x << endl;//这个x的作用是使得args推导的参数放到x里面,所以需要递归式推导
Print(args...);//可变参数作为变量进行传递时...是写在后面的
}
//Print(x, args...);有初学者很容易将递归式子写成这个样子,主要是因为函数有两个
//参数,但是这里的x只起到推导值存储的作用,所以可以看成x是不传的是有缺省值的,是由args产生的
//如果写成这样,那x永远都是之前推导的参数,会陷入死循环的(当args里面没有值的时候x就没有了)
//编译时递归推导解析参数
template<class... Args>
void showlist(Args... args)
{
Print(args...);
}
// 编译器会实例化生成下面这种格式
//void ShowList(int x, const char* y, double z)
//{
// Print(x, y, z);
//}
int main()
{
showlist(1, "wdwf", 1.1);
return 0;
}
以上我们是通过递归调用来实现了可变参数包的打印,有什么别的方法吗?
直接的for循环:(不行)
template<class... Args>
void showlist(Args... args)
{
cout << sizeof...(args);//计算args的大小(元素个数),没有问题,注意sizeof后面的...
for (size_t i = 0; i < sizeof...(args); i++)
{
cout << args[i] << endl;
}
// 可变参数模版编译时解析
// 上面是运行获取和解析,所以不支持这样用,这样写的话args没有展开,取不到里面的元素
}
由于可变参数模板都是在编译时解析的,上面的逻辑是在运行for循环时才读取解析,属于运行时解析。所以会使得无法展开此模板
所以有人就进行了如下可行的两种写法
法一:
template <class T>
int PrintArg(T t)
{
cout << t << " ";
return 0;
}
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { PrintArg(args)... };//不是传导args让t接收,是解析的args的里面的参数一个一个让t接收
//这样不仅编译时推导args还使得arr数组里面的元素类型统一
cout << endl;
}
//args编译推演生成下面的函数
void ShowList(int x, char y, std::string z)
{
int arr[] = { PrintArg(x),PrintArg(y),PrintArg(z) };//达到编译arr时进行解析了args的参数
cout << endl;
}
//还有一种写法:如下
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { (cout<<(args)<<" ", 0)...};//逗号表达式虽然最后是运算结果是后面这个参数的值,但是也会编译推导前面这个参数
//这样不仅编译时推导args还使得arr数组里面的元素类型统一
cout << endl;
}
//args编译推演生成下面的函数
void ShowList(int x, char y, std::string z)
{
int arr[] = { (cout<<(x)<<" ", 0), (cout << (y) << " ", 0), (cout << (z) << " ", 0) };
cout << endl;
}
可变参数模板在库里的应用+自己模拟实现
//args在库里面的应用主要是emplace_back,这个是新增的接口函数
//由于传入的是可以构造对于类型变量的参数包,所以底层是直接拿参数包
//进行构造了,就只需要调用构造函数,没有拷贝构造什么事了
//emplace_back总体而言会比push_back是更高效,推荐使用
//在我们自己的list里面需要自己写一下
int main()
{
bit::list<bit::string> lt;
// 左值
bit::string s1("111111111111");
lt.emplace_back(s1);
// 右值
lt.emplace_back(move(s1));
// 直接把构造string参数包往下传,直接用string参数包构造string
lt.emplace_back("111111111111");//args
lt.emplace_back(10, 'x');//args
cout << endl << endl;
bit::list<pair<bit::string, int>> lt1;
// 构造pair + 拷贝/移动构造pair到list的节点中data上
pair<bit::string, int> kv("苹果", 1);
lt1.emplace_back(kv);//args
lt1.emplace_back(move(kv));//args
cout << endl << endl;
// 直接把构造pair参数包往下传,直接用pair参数包构造pair
lt1.emplace_back("苹果", 1);//args
return 0;
}
如上我们先完成对可变参数模板的传值形式的写法,emplace_back的作用和push_back的作用基本一致的,都是尾插,所以既然要尾插一个string,就需要传入可以构建string的参数包。
string有多种构建方式,所以可以传多个参数包,完成传入之后我们开始写emplace_back的函数主体。
//emplace_back
template<class... Args>
void emplace_back(Args&&... args)
{
insert(end(), std::forward<Args>(args)...);//注意完美转发的写法
}
// 原理:编译器根据可变参数模板的解析过程可以生成对应参数的函数
/*void emplace_back(string& s)
{
insert(end(), std::forward<string>(s));
}
void emplace_back(string&& s)
{
insert(end(), std::forward<string>(s));
}
void emplace_back(const char* s)
{
insert(end(), std::forward<const char*>(s));
}
void emplace_back(size_t n, char ch)
{
insert(end(), std::forward<size_t>(n), std::forward<char>(ch));
}*/
emplace_back照样可以复用insert进行尾插,这里由于我们用来构造string而传的参数包都是典型的右值,所以每层传的参数都需要包证为右值(之前讲过了),但是之前用的都是move,这个会将原本是左值的东西直接转换成右值所以比较不好用,我们使用完美转发。
完美转发forward
先看一下forward的底层逻辑
template<typename T>
void PerfectForward(T&& t)//不是右值引用,不然会用&
{
//在模板里面的&&不是引用,只是单纯的通过t这个变量接收外面传来的值
// 模版实例化是左值引用,保持属性直接传参给Fun
// 模版实例化是右值引用,右值引用属性会退化成左值,转换成右值属性再传参给Fun
Fun(forward<T>(t));
}
//完美转发的简单底层逻辑如下,会预先创建多个可能匹配的传值类型的函数,当不同类型的值传进去时
//调用匹配的
//void PerfectForward(int& t)
//{
// Fun(t);
//}
//
//void PerfectForward(int&& t)
//{
// Fun(move(t));
//}
//
//void PerfectForward(const int& t)
//{
// Fun(t);
//}
//
//void PerfectForward(const int&& t)
//{
// Fun(move(t));
//}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
简单来说就是,完美转发会保持原本实参的左右值类型。
底层实现就这么简单吗,简化版是这样,但肯定没有这么简单的只是匹配对应的函数,我们可以转到定义进行观看:
接着自我实现emplace_back
接着完美转发时其保持右值之后,由于复用的insert,所以insert也需要重新写一个支持传入参数包的版本。
template<class... Args>
iterator insert(iterator pos, Args&&... args)
{
Node* cur = pos._node;
Node* newnode = new Node(std::forward<Args>(args)...);
Node* prev = cur->_prev;
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
然后由于构造Node结点也是使用的参数包传递的东西进行构造的,所以构造函数同样需要一个支持args的版本。
template<class... Args>
ListNode(Args... args)
:_next(nullptr)
, _prev(nullptr)
, _data(std::forward<Args>(args)...)
{}
这样List部分就完成了,有的同学就着急点运行了,你就会发现报了一个这种错误:
咦,怎么不支持,哦是string里面没有这种像如上传两个参数的构造函数,我们虽然完整的将构造string的参数包一步一步传到构造string这里了,就差构造通过这个传入n个数+字符类型的方式的参数包构造string了,结果string不支持或者没有这种构造方式,那无法构造了,因为找不到这种构造方式,库里支持这样的吗?
答案是肯定的,所以我们只需要加上这种构造方式就可以了。
string(size_t n, char ch = '\0')
{
cout << "string(size_t n, char ch = '\0')" << endl;
reserve(n);
for (int i = 0; i < n; i++)
{
_str[i] = ch;
}
_size = n;
_str[n] = '\0';
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];//开空间的时候多开了一个
if (_str)
{
strcpy(tmp, _str);
delete[] _str;
}
_str = tmp;
_capacity = n;
}
}
下面的运行结果:
我们会发现所以传值传的是构造string的参数包(string的构造方法)的都只进行了构造没有拷贝,反而传的是类对象的都进行了拷贝,所以传args参数包的方式的效率比较高和参数包是不断往下传递的并且会直接拿参数包进行构造都得到了证实。由此可见emplace_back的效率之高。