目录
一、完美转发
1.1模板中的&&万能引用
1.2完美转发
1.3完美转发实际中的引用场景
二、新的类功能
2.1移动构造和移动赋值规则详解
2.2类成员变量初始化和强制生成默认函数(default)
2.3禁止生成默认函数的关键字(delete)
三、可变参数模板
3.1递归函数方式展开参数包
3.2逗号表达式展开参数包
3.3可变参数模板使用场景(emplace优势分析)
一、完美转发
在上一篇章中,学习了右值引用,我们知道右值引用基本上只能接收右值,对于左值加上move函数才能接收,但是这一章节重点不是在这里,而是当&&用在模板当中就不再代表右值引用了,而是万能引用 ,其既能接收左值又能接收右值。同时,改万能引用又存在一定的缺陷,不管该万能引用变量接收的是左值还是右值,要使用该引用变量,其都会被当成左值,就失去了原来的含义了。如下代码:
1.1模板中的&&万能引用
#include <iostream>
using namespace std;
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t)//模板中的&&是万能引用
{
Fun(t);//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;
}
输出结果:
那么为了在传递过程中能够保持原来的属性,就可以使用完美转发。
1.2完美转发
std::forward完美转发在传参的过程中保留对象原生类型属性
那么对于上述代码,只需对需要保持原属性的变量进行完美转发,其他代码不变,如下:
Fun(forward<T>(t));//完美转发,保持t的原生属性
输出结果:
1.3完美转发实际中的引用场景
采用完美转发,可以保持引用变量拥有原来的属性,若是右值,当要进行拷贝时,就可以调用移动构造,来减少拷贝构造。例如:
#include <iostream>
using namespace std;
template<class T>
struct ListNode
{
ListNode* _next = nullptr;
ListNode* _prev = nullptr;
T _data;
};
template<class T>
class List
{
typedef ListNode<T> Node;
public:
List()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
}
void PushBack(T&& x)
{
//Insert(_head, x);
Insert(_head, std::forward<T>(x));//保持x原来的属性
}
void PushFront(T&& x)
{
//Insert(_head->_next, x);
Insert(_head->_next, std::forward<T>(x));//保持x原来的属性
}
void Insert(Node* pos, T&& x)
{
Node* prev = pos->_prev;
Node* newnode = new Node;
newnode->_data = std::forward<T>(x); // 插入的时候也能够保持原来的属性,就可以调用移动构造,减少深拷贝
// prev newnode pos
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
}
void Insert(Node* pos, const T& x)
{
Node* prev = pos->_prev;
Node* newnode = new Node;
newnode->_data = x; // 关键位置
// prev newnode pos
prev->_next = newnode;
newnode->_next = pos;
pos->_prev = newnode;
}
private:
Node* _head;
};
int main()
{
List<string> lt;
lt.PushBack("1111");
lt.PushFront("2222");
return 0;
}
二、新的类功能
2.1移动构造和移动赋值规则详解
上一篇章,我们知道c++11中增加了两个默认成员函数,移动构造函数和移动赋值运算符重载函数。相应的对于这两个函数也围绕着一定的规则。
- 如果没有自己实现移动构造函数,且没有实现析构函数、拷贝构造、赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造,该移动构造,对于内置类型成员会执行逐成员按字节拷贝,对于自定义类型成员,则需要看这个成员是否实现移动构造,若实现,则调用移动构造,否则调用拷贝构造。
- 如果没有自己实现移动赋值重载函数,且没有实现析构函数、拷贝构造、赋值重载中的任意一个。那么编译器会自动生成一个默认移动赋值,该移动赋值,对于内置类型成员会执行逐成员按字节拷贝,对于自定义类型成员,则需要看这个成员是否实现移动赋值,若实现,则调用移动赋值,否则调用拷贝赋值。
- 如果自己实现了移动构造或者移动赋值,编译器便不会自动提供拷贝构造和拷贝赋值
为什么条件会如此苛刻?
思考一下,会发现,这并不是凭空这么严格,移动构造、移动赋值针对的是深拷贝,若是自己实现了移动构造、移动赋值,从推理来说,这是解决深拷贝,就没必要去实现拷贝构造、拷贝赋值,编译器也没必要去提供。反过来,若没有实现移动构造、移动赋值,说明是一些浅拷贝,若也没有特定的意向去指定浅拷贝怎么实现去向(析构、拷贝构造、赋值重载),那么可以由编译器默认生成的移动构造来实现浅拷贝。 同理,对于自定义类型也一样。
通过一段代码来展示上述的理论:
#include <iostream>
using namespace std;
class Student
{
public:
Student(const char* name = "", int age = 0)
:_name(name)
,_age(age)
{}
//提供了移动构造,那么编译器不会自动提供拷贝构造和拷贝赋值,所以得自己提供拷贝构造和拷贝赋值
Student(Student&& s)
:_name(move(s._name))
,_age(s._age)
{}
// Copy constructor
Student(const Student& s)
: _name(s._name)
, _age(s._age)
{}
// Copy assignment operator
Student& operator=(const Student& s)
{
if (this != &s)
{
_name = s._name;
_age = s._age;
}
return *this;
}
private:
string _name;
int _age;
};
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
,_stu("张三",13)
{}
private:
string _name;
int _age;
Student _stu;//自定义类型成员
};
int main()
{
Person s1;
Person s2 = s1;//对于Person类,没有移动构造,析构,拷贝构造,赋值重载。将s1拷贝给s2,对于内置类型就就是按成员逐字节拷贝,对于自定义(_stu)
//,由于s1是左值,会去调用它的拷贝构造
//同理,move(s1)是右值,由于自定义类型中实现了移动构造,就去调用移动构造,否则调用拷贝构造
Person s3 = move(s1);
//同理,move(s2)是右值,由于自定义类型中未实现移动赋值,就去调用拷贝赋值重载
Person s4;
s4 = move(s2);
return 0;
}
调试监口:
上述结果还需小伙伴自己动手观察现象才能更加清楚的明白其中的道理。
2.2类成员变量初始化和强制生成默认函数(default)
c++11允许在类定义时给成员变量初识缺省值,默认生成构造函数会使用这些缺省值初始化,这个我们在类和对象默认就讲了,这里就不在细讲了。
我们知道default可以强制生成默认构造函数,对于提供了拷贝构造,编译器就不会提供移动构造了,如果要生成移动构造,那么对此就可以使用default
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
,_stu("张三",13)
{}
Person(const Person& p)
:_name(p._name)
, _age(p._age)
, _stu(p._stu)
{}
Person& operator=(const Person& p)
{
if (this != &p)
{
_name = p._name;
_age = p._age;
_stu = p._stu;
}
}
Person(Person&& p) = default;//强制生成默认移动构造,相应的,编译器就不会默认生成拷贝构造,赋值重载,而main函数自己写的代码中又要用到这两个构造函数,那么就需要自己提供拷贝构造、赋值重载
private:
string _name;
int _age;
Student _stu;
};
2.3禁止生成默认函数的关键字(delete)
在c++98要想限制某些默认函数的生成,可以将该函数设置为private,并且只声明不定义,对于其他人想要调用就会报错。在c++11中,只需在声明加上=delete即可,告诉编译器不生成对应的默认函数,称该函数被删除。
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
, _stu("张三", 13)
{}
Person(const Person& p) = delete;
private:
string _name;
int _age;
Student _stu;
};
对于新的功能还有override和final,这里就不在多说了,都在多态章节中讲过了。
override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。其加在函数重写后面
final:修饰虚函数,表示该虚函数不能再被重写。这就隔绝了函数重写可能带来的错误。
三、可变参数模板
c++11的新特性可变参数模板是一个可以接收多个参数的模板,其中参数类型是...Args,用该参数类型定义一个参数包的方式为Args ...args,把带省略号的参数叫做模板参数包,它里面包含了0到N个模板参数。对于可变参数模板特性,这里只能学点皮毛,要想深入得另外循序渐进。
定义形式:
template <class ...Args>
void 函数名(Args... args){}
//其中,中间的省略号是连着的,但该串省略号可以不与旁边的字符串连着,只要在两者之间即可,例如:class ... Args Args ... args
对于可以包含这么多个模板参数到底是什么意思呢?又该如何获取里头的参数?
参数包的意思是可以通过外面传递多个参数给这个参数包,当要获取里头的参数时,就可以通过展开参数包来获取,虽然这是获取参数的方式,但同时也是难点,对于展开参数包,这里有两种方式。
3.1递归函数方式展开参数包
#include <iostream>
using namespace std;
template <class T>
void ShowList(const T& t)
{
cout << t << endl;
}
// 递归展开参数包
template <class T, class ...Args>
void ShowList(T value, Args... args)//参数value表示第1个参数,后面的参数包是从第2个参数开始(更符合规范)
{
cout << value << " ";
ShowList(args...);//若参数大于1个,就递归该多参数函数,若为1就调用上面单参函数。每次递归会取到当前参数交给value,直到剩一个参数会调用单参数函数
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
输出结果:
3.2逗号表达式展开参数包
#include <iostream>
using namespace std;
template <class T>
void ShowList(const T& t)
{
cout << t << endl;
}
// 逗号表达式展开参数包
template <class ...Args>
void ShowList( Args... args)
{
((cout << args << " "), ...);// 该逗号表达式是一个折叠表达式,它会依次展开参数包 args,(cout << args2 << " ",cout << args3 << " ",...)
cout << endl;
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
输出结果:
其中折叠表达式需要用到/std:c++17。那么如何设置?
点击项目,打开属性,到此页面,找到c/c++->语言->c++语言标准(默认是c++14标准)->选择c++17标准,应用确定即可
3.3可变参数模板使用场景(emplace优势分析)
对于可变参数模板,用到的就是STL容器的emplace系列接口,之前不提,是因为不了解,那么现在有了基础,可以尝试来理解。例如:
emplace系列借口也是插入接口,支持可变参数,还有万能引用。那么相对insert,emplace有什么优势,在用法上有什么区别吗?
我们拿出自己的list类并进行修改,添加对应的右值版本,主要是节点、插入的添加,相应的左值需要通过move变右值来满足参数传递,以及通过forward进行完美转发维持原来的属性。结合自己的string类,来观察emplace现象。
list.h:
//list.h
#pragma once
#include <iostream>
#include <vector>
using namespace std;
namespace bit
{
template<class T>
struct list_node
{
T _data;
list_node<T>* _next;
list_node<T>* _prev;
list_node(const T& x = T())
:_data(x)
, _next(nullptr)
, _prev(nullptr)
{}
//右值引用版本
list_node(T&& x)
:_data(move(x))
, _next(nullptr)
, _prev(nullptr)
{}
//参数包版本
template <class... Args>
list_node(Args&&... args)
: _data(args...)
, _next(nullptr)
, _prev(nullptr)
{}
};
// T T& T*
// T cosnt T& const T*
template<class T, class Ref, class Ptr>
struct __list_iterator
{
typedef list_node<T> Node;
typedef __list_iterator<T, Ref, Ptr> self;
Node* _node;
__list_iterator(Node* node)
:_node(node)
{}
self& operator++()
{
_node = _node->_next;
return *this;
}
self& operator--()
{
_node = _node->_prev;
return *this;
}
self operator++(int)
{
self tmp(*this);
_node = _node->_next;
return tmp;
}
self operator--(int)
{
self tmp(*this);
_node = _node->_prev;
return tmp;
}
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
bool operator!=(const self& s)
{
return _node != s._node;
}
bool operator==(const self& s)
{
return _node == s._node;
}
};
template<class T>
class list
{
typedef list_node<T> Node;
public:
typedef __list_iterator<T, T&, T*> iterator;
typedef __list_iterator<T, const T&, const T*> const_iterator;
//typedef __list_const_iterator<T> const_iterator;
const_iterator begin() const
{
return const_iterator(_head->_next);
}
const_iterator end() const
{
return const_iterator(_head);
}
iterator begin()
{
//return iterator(_head->_next);
return _head->_next;
}
iterator end()
{
//return iterator(_head->_next);
return _head;
}
void empty_init()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
_size = 0;
}
list()
{
empty_init();
}
// lt2(lt1)
list(const list<T>& lt)
{
empty_init();
for (auto e : lt)
{
push_back(e);
}
}
void swap(list<T>& lt)
{
std::swap(_head, lt._head);
std::swap(_size, lt._size);
}
// lt3 = lt1
list<int>& operator=(list<int> lt)
{
swap(lt);
return *this;
}
~list()
{
clear();
delete _head;
_head = nullptr;
}
void clear()
{
iterator it = begin();
while (it != end())
{
it = erase(it);
}
}
void push_back(const T& x)
{
insert(end(), x);
}
void push_back(T&& x)
{
insert(end(), forward<T>(x));
}
template <class... Args>
void emplace_back(Args&&... args)
{
Node* newnode = new Node(args...);
// 链接节点。。。
}
void push_front(const T& x)
{
insert(begin(), x);
}
void pop_front()
{
erase(begin());
}
void pop_back()
{
erase(--end());
}
iterator insert(iterator pos, const T& x)
{
Node* cur = pos._node;
Node* newnode = new Node(x);
Node* prev = cur->_prev;
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
++_size;
return iterator(newnode);
}
iterator insert(iterator pos, T&& x)
{
Node* cur = pos._node;
Node* newnode = new Node(forward<T>(x));
Node* prev = cur->_prev;
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
++_size;
return iterator(newnode);
}
iterator erase(iterator pos)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* next = cur->_next;
delete cur;
prev->_next = next;
next->_prev = prev;
--_size;
return iterator(next);
}
size_t size()
{
return _size;
}
private:
Node* _head;
size_t _size;
};
}
test.cpp:
//test.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#pragma warning(disable:6031)
#include "list.h"
#include <assert.h>
namespace bit
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
//cout << "string(char* str) -- 构造" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// s1.swap(s2)
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 移动构造
string(string&& s)
{
cout << "string(string&& s) -- 移动拷贝" << endl;
swap(s);
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(const string& s) -- 深拷贝" << endl;
/*string tmp(s);
swap(tmp);*/
if (this != &s)
{
char* tmp = new char[s._capacity + 1];
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s)-- 移动赋值" << endl;
swap(s);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
//string operator+=(char ch)
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0; // 不包含最后做标识的\0
};
bit::string to_string(int x)
{
bit::string ret;
while (x)
{
int val = x % 10;
x /= 10;
ret += ('0' + val);
}
reverse(ret.begin(), ret.end());
return ret;
}
}
int main()
{
bit::list<bit::string> lt;//深拷贝
bit::string s1("hello world");
cout << endl;
lt.push_back(s1);//深拷贝
lt.push_back(bit::to_string(1234));//移动构造+移动构造
cout << endl;
lt.push_back("1122");//移动构造
return 0;
}
输出结果:
由上述结果结合代码分析,验证了代码的正确性,接下来进行emplace的实验
int main()
{
bit::list<bit::string> lt;//深拷贝
cout << endl;
lt.push_back("xxxx");//构造+移动构造
lt.emplace_back("1122");//直接构造
return 0;
}
输出结果:
将string中的构造的注释给打开,由结果分析,对于传递单个参数而言,emplace_back是直接将字符串给构造了,并没有进行移动构造。这是为何,再来看看传递多参数。
int main()
{
bit::list<pair<bit::string,int>> lt;//深拷贝
cout << endl;
lt.push_back(make_pair("111", 1));
lt.emplace_back("20", 2);
return 0;
}
输出结果:
由结果分析,emplace_back依然是直接构造,因为emplace_back传递的是参数包,会一层一层往下传,最后直接调用data的构造,如图:
通过以上的实验,可以发现,emplace对比insert仅仅减少了一个移动构造,而移动构造的时间复杂度并不多高,所以emplace确实比insert好一点,但是并不是说好到哪里去,当然这是深拷贝的情况,但是对于浅拷贝就不一样了,当浅拷贝比较大时,对于insert来说时间复杂度较高,而对于emplace直接构造才会有更大的优势。
end~