7.3 完美转发
模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
template<class T>
void PerfectForward(T&& t)
{
}
就是说如果传的是左值他就是左值引用,如果传的是右值它就是右值引用。
用 typeid 是查看不了的,它只会打出你本来的类型
那么能不能通过函数调用来查看 ?
查看不了
因为模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力, 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值。
如果希望能够在传递过程中保持它的左值或者右值的属性, 就需要用到完美转发。
std::forward 完美转发在传参的过程中保留对象原生类型属性
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<class T>
void PerfectForward(T&& t)
{
Fun(forward<T>(t));
}
int main()
{
PerfectForward(10);//右值
int a = 10;
PerfectForward(a);//左值
const int b = 10;
PerfectForward(b);//const 左值
PerfectForward(move(b));// const 右值
return 0;
}
完美转发实际中的使用场景:
template<class T>
struct ListNode
{
ListNode<T>* _next;
ListNode<T>* _prev;
T _date;
ListNode(const T& pos = T())
:_date(pos)
{
_next = nullptr;
_prev = nullptr;
}
ListNode(T&& pos)
:_date(move(pos))
{
_next = nullptr;
_prev = nullptr;
}
};
template<class T>
class list
{
public:
typedef ListNode<T> node;
list()
:_head(new node)
{
_head->_next = _head;
_head->_prev = _head;
}
void push_back(T&& x)
{
//Insert(_head, x);
Insert(_head, std::forward<T>(x));
}
void Insert(node* pos, T&& x)
{
node* prev = pos->_prev;
node* newnode = new node(move(x));//因为T的类型在创建这个类的时候就已经确定,所
//以可以直接 move(),也可以使用完美转发
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
}
iterator insert(iterator pos, const T& x)
{
node* newnode = new node(x);
node* next = pos._node;
node* last = next->_prev;
last->_next = newnode;
newnode->_prev = last;
newnode->_next = next;
next->_prev = newnode;
return iterator(newnode);
}
private:
node* _head;
};
8 新的类功能
原来C++类中,有6个默认成员函数:
1. 构造函数
2. 析构函数
3. 拷贝构造函数
4. 拷贝赋值重载
5. 取地址重载
6. const 取地址重载
最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。C++11 新增了两个:移动构造函数和移动赋值运算符重载。
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
如果你没有自己实现移动构造函数,且析构函数 、拷贝构造、拷贝赋值重载中的任意一个都没有实现。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
如果你没有自己实现移动赋值重载函数,且析构函数 、拷贝构造、拷贝赋值重载中的任意一个都没有实现。那么编译器会自动生成一个默认移动赋值。默认生成的移动赋值函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
需要显示写析构,说明有资源需要释放
1、说明需要显示写拷贝构造和赋值重载
2、说明需要显示写移动构造和移动赋值
一般情况下编译器默认生成的移动构造是没有什么意义的。但是如上面的person 类,则就很有意义因为里面有一个自定义类型的成员他写了移动构造,person 类自动生成的移动构造就会调用自定义类型成员的移动构造。
所以看似移动构造默认生成的条件苛刻,实际是合理的,因为这三个函数(析构函数 、拷贝构造、拷贝赋值重载)本来就是一体的。
类成员变量初始化
初始化列表:
以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟
一个放在括号中的初始值或表达式。
每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
所有的成员你可以在初始化列表初始化也可以在函数体内初始化但是下面三种成员,必须放在初始化列表位置进行初始化:
- 引用成员变量
- const成员变量
- 自定义类型成员(且该类没有默认构造函数时)
如果放到函数体则会报错
const 变量有一个特点就是必须在定义的时候初始化 ,因为const 变量只有一次初始化的机会,
而每个成员定义的的地方在初始化列表。
引用成员变量也必须在定义时初始化,总不能定义一个不知道是谁的别名吧!
所以初始化列表本质可以理解为每个对象中成员定义的地方。
尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,
一定会先使用初始化列表初始化。
强制生成默认函数的关键字default:
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。
编译器会觉得这些函数是一体的所以要一起写出来。
禁止生成默认函数的关键字delete:
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
不想让拷贝则可以加一个delete
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
//自己生成移动构造和拷贝构造
Person(const Person& p) = delete;
private:
bit::string _name;
int _age;
};
在C++98没有delete时可以如下操作:
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
//只声明不实现, 声明为私有
private:
Person(const Person& p);
Person& operator=(const Person& p);
private:
bit::string _name;
int _age;
};
io流就是一个不想被拷贝的类。
template <class ...Args>
void ShowList(Args&&... args)//传左值变左值引用,传右值变右值引用
{}
9 可变参数模板
C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。
下面就是一个基本可变参数的函数模板。
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
名字是随便取的
template <class ...Args>
void ShowList(Args&&... args)//传左值变左值引用,传右值变右值引用
{}
可变模板参数 = 参数类型可变 && 参数个数可变
这样写是错误的,C语言的可变参数底层是类似这样的方式支持的,但是C++不行。C语言取可变参数是在运行时去取,而C++ 可变模板参数是在编译时,要解析每个参数。而for 循环是运行时获取和解析,所以这样是走不通的。
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值。
方式1
编译时递归推导解析
void print()
{
cout << endl;
}
template <class T, class ...Args>
void print(T t, Args... args)
{
cout << t << " ";
print(args...);
}
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{
print(args...);
}
int main()
{
ShowList();
ShowList(1);
ShowList(1, "11111");
ShowList(1, "11111", 3);
}
编译器底层的逻辑就像下面这样
void print()
{
cout << endl;
}
//template <class T, class ...Args>
//void print(T t, Args... args)
//{
// cout << t << " ";
// print(args...);
//}
void print(int a)
{
cout << a << " ";
print();
}
void print(const char* s, int a)
{
cout << s << " ";
print(a);
}
void print(int i, const char* s, int a)
{
cout << i << " ";
print(s, a);
}
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
//template <class ...Args>
//void ShowList(Args... args)
//{
// print(args...);
//}
void ShowList(int i, const char * s, int a)
{
print(i, s, a);
}
int main()
{
//ShowList();
//ShowList(1);
//ShowList(1, "11111");
ShowList(1, "11111", 3);
}
可变参数模板帮助我们省略了很多模板。
方式2
逗号表达式展开参数包
这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)...}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc... ),最终会创建一个元素值都为0的数组int arr[sizeof...(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。
template <class T>
void PrintArg(T t)
{
cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { (PrintArg(args), 0)... };
cout << endl;
}
int main()
{
ShowList(1);
ShowList(1, "11111");
ShowList(1, "11111", 3);
}
也可以用这种写法:与上面是等价的。
template <class T>
int PrintArg(T t)
{
cout << t << " ";
return 0;
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { PrintArg(args)... };
cout << endl;
}
int main()
{
ShowList(1);
ShowList(1, "11111");
ShowList(1, "11111", 3);
}
我们像下面可以这样理解。
还可以这样写:
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { (cout<<args<<" ", 0)...};
cout << endl;
}
int main()
{
ShowList(1);
ShowList(1, "11111");
ShowList(1, "11111", 3);
}
emplace_back 的使用及实现
int main()
{
list<bit::string> l1;
bit::string s1("111111");
l1.emplace_back(s1);
//l1.emplace_back(s1, s1, s1);//虽然它是可变参数模板但是它不支持插入多个值
l1.emplace_back(move(s1));
return 0;
}
那么emplace_back 使用可变模板参数有什么用呢?
可以直接传list<T> T对象的参数包,直接用参数包构造T
int main()
{
list<pair<bit::string, int>> l2;
//构造pair 拷贝/移动构造pair 到list节点的data 上
pair<bit::string, int> p1("苹果", 1);
l2.emplace_back(p1);
l2.emplace_back(move(p1));
l2.emplace_back("苹果", 1);
return 0;
}
拷贝构造和赋值重载改成下面那样了更好观察
// 拷贝构造
// s2(s1)
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(const string& s) -- 深拷贝" << endl;
if (this != &s)
{
_str[0] = '\0';
_size = 0;
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
return *this;
}
template<class T>
struct ListNode
{
ListNode<T>* _next;
ListNode<T>* _prev;
T _date;
template<class... Args>
ListNode(Args&&... args)
:_date(forward<Args>(args)...)
{
_next = nullptr;
_prev = nullptr;
}
};
template<class T>
class list
{
public:
template<class... Args>
void emplace_back(Args&&... args)
{
insert(end(), forward<Args>(args)...);
}
template<class... Args>
iterator insert(iterator pos, Args&&... args)
{
node* newnode = new node(forward<Args>(args)...);
node* next = pos._node;
node* last = next->_prev;
last->_next = newnode;
newnode->_prev = last;
newnode->_next = next;
next->_prev = newnode;
return iterator(newnode);
}
private:
node* _head;
};
这里用完美转发的原因还是因为如果传的右值,还是会退化为左值。
感谢大家的观看!