从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(中):https://developer.aliyun.com/article/1522391
4. 完美转发
4.1 万能引用(引用折叠)
写多个重载函数,根据实参类型调用不同函数。
- 形参类型分别是左值引用,const左值引用,右值引用,const右值引用:
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; } // 万能引用(引用折叠):t既能引用左值,也能引用右值 template<typename T> void PerfectForward(T&& t) { Fun(t); // 此时t变成了左值/const左值 } 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; }
代码中的perfectForward函数模板被叫做万能引用模板,无论调用该函数时传的是什么类型,它都能推演出来:
在函数模板推演的过程中会发生引用折叠:模板参数T&&中的两个&符号折叠成一个。当传入的实参是左值时,就会发生引用折叠,是右值时就不会发生引用折叠。
无论传的实参是什么,都不用改变模板参数T&&,编译器都能够自己推演。
这就是万能引用,只需要一个模板就可以搞定,不需要分类去写。
上面万能模板中,虽然推演出来了各自实参类型,但是由于右值引用本身是左值属性,所以需要使用move改变属性后才能调用对应的重载函数。
有没有办法不用move改变左值属性,让模板函数中的t保持它推演出来的类型。答案是有的,完美转发就能够保持形参的属性不变。
4.2 完美转发forward
完美转发同样是C++11提供的,它也是一个模板:
完美转发:完美转发在传参的过程中保留对象原生类型属性。实参传递过来后,推演出的形参是什么类型就保持什么类型继续使用。
这里会语法就行:
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; } // 万能引用(引用折叠):t既能引用左值,也能引用右值 template<typename T> void PerfectForward(T&& t) { Fun(std::forward<T>(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; }
此时再使用万能引用的时候,在函数模板中调用重载函数时只需要使用完美转发就可以保持推演出来的属性不变,右值引用仍然是右值,const右值引用也仍然是右值。
需要注意的是:
虽然右值不可以被修改,但是右值引用以后具有了左值属性,才能被转移,一旦被const修饰以后就无法转移了。所以在使用右值引用的时候,不要使用const来修饰。
5. 新的类功能
在原来的C++类中,有6大默认成员函数:
1. 构造函数 2. 析构函数 3. 拷贝构造函数 4. 拷贝赋值重载 5. 取地址重载 6. const 取地址重载
重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的,而且完全符号我们使用的需求。
5.1 默认生成的移动构造/赋值
C++11中新增了两个:移动构造和移动赋值运算符重载,此时C++11一共有8个默认成员函数了。
这两个成员函数在前面已经介绍过了,这里站在默认成员函数的角度继续谈谈。
满足下列条件,编译器会自定生成移动构造函数:
没有自己显示定义移动构造函数,且没有实现析构函数,拷贝构造函数,拷贝赋值重载中的任何一个。
此时编译器会自定生成一个默认的移动构造函数。功能:
默认生成的移动构造函数,对于内置类型会逐字节进行拷贝。
对于自定义类型,如果实现了移动构造就调用移动构造,没有实现就调用拷贝构造。
满足下列条件,编译器会自动生成移动赋值重载函数
自己没有显示定义移动赋值重载函数。且没有实现析构函数,拷贝构造函数,拷贝赋值重载中的任何一个。
此时编译器会自动生成一个默认移动赋值函数。功能:
对于内置类型会按字节拷贝。
对于自定义类型,如果实现了移动赋值就调用移动赋值,如果没有实现就调用拷贝赋值。
创建一个类,屏蔽掉拷贝构造,拷贝赋值,以及析构函数,成员变量有一个是我们自己实现的string,里面有移动构造和移动赋值。
namespace rtx { class string { public: string(const char* str = "") :_size(strlen(str)) , _capacity(_size) { _str = new char[_capacity + 1]; strcpy(_str, str); } void swap(string& s) { ::swap(_str, s._str); ::swap(_size, s._size); ::swap(_capacity, s._capacity); } string(const string& s) // 拷贝构造 :_str(nullptr) , _size(0) , _capacity(0) { cout << "string(const string& s) -- 拷贝构造(深拷贝)" << endl; string tmp(s._str); swap(tmp); } string(string&& s) // 移动构造 :_str(nullptr) , _size(0) , _capacity(0) { cout << "string(string&& s) -- 移动构造(资源转移)" << endl; swap(s); } string& operator=(const string& s) // 拷贝赋值 { cout << "string& operator=(string s) -- 拷贝赋值(深拷贝)" << endl; string tmp(s); swap(tmp); return *this; } string& operator=(string&& s) // 移动赋值 { cout << "string& operator=(string s) -- 移动赋值(资源移动)" << endl; swap(s); return *this; } ~string() { delete[] _str; _str = nullptr; } protected: char* _str; size_t _size; size_t _capacity; }; } class Person { public: //Person(const char* name = "", int age = 0) // :_name(name) // , _age(age) //{} //Person(const Person& p) // 拷贝构造 // :_name(p._name) // , _age(p._age) //{} //Person& operator=(const Person& p) // 拷贝赋值 //{ // if (this != &p) // { // _name = p._name; // _age = p._age; // } // return *this; //} //~Person() //{} protected: rtx::string _name; int _age; }; int main() { Person s1; Person s2 = s1; Person s3 = std::move(s1); Person s4; s4 = std::move(s2); return 0; }
此时Person就自动生成了移动构造函数,并且调用了string中的移动构造和移动赋值函数来构造string对象。
将Person中的拷贝构造,拷贝赋值,析构函数任意放出一个来。(这里只放出了析构)使用右值构建string对象时,都会调用string的拷贝构造和拷贝赋值函数。
- 编译器默认生成的移动赋值和移动构造类型。
- 如果符合条件就生成,内置类型按字节处理,自定义类型调用自定义类型的移动赋值或者移动构造,如果没有的化就调用它们的拷贝赋值或者拷贝构造。
- 如果不符合条件,就直接调用自定义类型的拷贝复制或者拷贝构造。
5.2 类里新的关键字
强制生成默认函数的关键字default:
这个default并不是switch中的default,而是C++11的新用法。
- 假设类中的某个默认成员函数没有自动生成,但是我们需要它,就可以用default,强制让编译器自动生成默认函数。
5.1里的代码:将Person中的拷贝构造,拷贝复制,析构函数都显示定义,此时就破坏了自动生成移动构造的条件。把Person里的注释放开,使用default强制生成默认的移动构造函数
从结果中可以看到,仍然调用了string中的移动构造函数,而不是调用的拷贝构造(深拷贝)。
- 说明Person中仍然生成了默认的移动构造函数。
禁止生成默认成员函数的关键字delete:
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁 已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即 可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
C++98不生成默认成员函数的方法:直接一个分号(要放到保护或者私有里,这里就不放了)
在Person类中不显示定义拷贝构造函数,拷贝复制函数,析构函数,此时符合自动生成默认移动构造的条件。 声明移动构造函数,但是没有定义(要放到保护或者私有里,防止类外实现,这里就不放了)。此时在编译的时候就会报错,这是C++98中的方式,利用链接时找不到函数的定义报错。C++11就新增delete关键字使其在编译阶段就报错:
- C++11中,使用delete同样可以实现不让自动生成默认成员函数。
同样在编译时报错了。编译器会自动生成移动构造函数,但是此时使用了delete,编译器就会报错,告诉我们这里生成了移动构造。这是为了在编译阶段就报错,而不是运行时再报错。
以前提到的一道题:
// 要求delete关键字实现,一个类,只能在堆上创建对象 class HeapOnly { public: HeapOnly() { _str = new char[10]; } ~HeapOnly() = delete; //void Destroy() // 如果要销毁只能这样 //{ // delete[] _str; // operator delete(this); //} private: char* _str; //... };
继承和多态中的final与override关键字
这两个关键字在继承和多态部分详细讲解过,这里不再详细讲解。
final
- 在继承中,被final修饰的类叫做最终类,是无法继承的。
- 在多态中,被final修饰的虚函数是无法进行重写的。
override
- 在多态中,用来检查虚函数是否完成了重写。
本篇完。
C++11中的很多东西虽然让C++越来越不像C++,比如列表初始化等内容,但是还是有一些非常有用的东西的:比如今天讲到的右值引用,和下一篇学的lambda表达式。
下一篇:从C语言到C++_34(C++11_下)可变参数+ lambda+function+bind+笔试题。