概述
class A {};A &a = A(); // 错误A && a2 = A(); // 正确
从抽象层面理解三种传参方式:
- 复制传参(
T):把你的东西一模一样地给我弄一份,之后我是我,你是你,互不影响 - 引用传参(
T&):我就是你,所有对我的操作都是对你的操作 - 移动传参(
T&&):我把你抢了,你啥都不剩了(实参的所有资源都给形参了,实参只剩一个空壳)
移动传参比复制传参好在哪?复制传参需要把所有底层资源都重新初始化并复制一遍(如动态分配的内存),移动传参不需要,直接抢实参的。
std::move()函数接收一个左值,返回他的右值
int a = 1; // 左值int &b = a; // 左值引用// 移动语意: 转换左值为右值引用int &&c = move(a);void printInt(int &i) {cout << "lval ref: " << i << endl;}void printInt(int &&i) {cout << "rval ref: " << i << endl;}int main() {int i = 1;// 调用 printInt(int&), i是左值printInt(i);// 调用 printInt(int&&), 6是右值printInt(6);// 调用 printInt(int&&),移动语意printInt(std::move(i));}
使用
#include <iostream>#include <cstring>#include <vector>using namespace std;class MyString {public:static size_t CCtor; //统计调用拷贝构造函数的次数static size_t MCtor; //统计调用移动构造函数的次数static size_t CAsgn; //统计调用拷贝赋值函数的次数static size_t MAsgn; //统计调用移动赋值函数的次数public:// 构造函数explicit MyString(const char *cstr = nullptr) {if (cstr) {m_data = new char[strlen(cstr) + 1];strcpy(m_data, cstr);} else {m_data = new char[1];*m_data = '\0';}}// 拷贝构造函数MyString(const MyString &str) {CCtor++;m_data = new char[strlen(str.m_data) + 1];strcpy(m_data, str.m_data);}// 移动构造函数MyString(MyString &&str) noexcept: m_data(str.m_data) {MCtor++;str.m_data = nullptr; //不再指向之前的资源了,防止资源被释放两次}// 拷贝赋值函数 =号重载MyString &operator=(const MyString &str) {CAsgn++;if (this == &str) // 避免自我赋值!!return *this;char *tmp = m_data;m_data = new char[strlen(str.m_data) + 1];delete[] m_data;strcpy(m_data, str.m_data);return *this;}// 移动赋值函数 =号重载MyString &operator=(MyString &&str) noexcept {MAsgn++;if (this == &str) // 避免自我赋值!!return *this;delete[] m_data;m_data = str.m_data;str.m_data = nullptr; //不再指向之前的资源了return *this;}~MyString() {delete[] m_data;}char *get_c_str() const { return m_data; }private:char *m_data;};size_t MyString::CCtor = 0;size_t MyString::MCtor = 0;size_t MyString::CAsgn = 0;size_t MyString::MAsgn = 0;template<typename T>void move_swap(T &t1, T &t2) // 移动交换{T tmp(move(t1));t1 = move(t2);t2 = move(tmp);}int main() {vector<MyString> vecStr;vecStr.reserve(1000); //先分配好1000个空间for (int i = 0; i < 1000; i++) {vecStr.push_back(MyString("hello"));}cout << "CCtor = " << MyString::CCtor << endl;cout << "MCtor = " << MyString::MCtor << endl;cout << "CAsgn = " << MyString::CAsgn << endl;cout << "MAsgn = " << MyString::MAsgn << endl;cout << "*******************************" << endl;MyString s1("world"), s2("hello");MyString::CCtor = 0;MyString::MCtor = 0;MyString::CAsgn = 0;MyString::MAsgn = 0; // 记录清零move_swap(s1, s2);cout << s1.get_c_str() << s2.get_c_str() << endl;cout << "CCtor = " << MyString::CCtor << endl;cout << "MCtor = " << MyString::MCtor << endl;cout << "CAsgn = " << MyString::CAsgn << endl;cout << "MAsgn = " << MyString::MAsgn << endl;}/* 结果CCtor = 0MCtor = 1000CAsgn = 0MAsgn = 0*******************************helloworldCCtor = 0MCtor = 1CAsgn = 0MAsgn = 2*/
不完美转发
#include <iostream>#include <cstring>#include <vector>using namespace std;void process(int& i){cout << "process(int&):" << i << endl;}void process(int&& i){cout << "process(int&&):" << i << endl;}void myforward(int&& i){cout << "myforward(int&&):" << i << endl;process(i);}int main(){int a = 0;process(a); //a被视为左值 process(int&):0process(1); //1被视为右值 process(int&&):1process(move(a)); //强制将a由左值改为右值 process(int&&):0myforward(2); //右值经过forward函数转交给process函数,却成为了一个左值,// 原因是该右值有了名字 所以是 process(int&):2myforward(move(a)); // 同上,在转发的时候右值变成了左值 process(int&):0}
universal references(通用引用)
template<typename T>void f( T&& param){}f(10); //10是右值int x = 10; //f(x); //x是左值
如果上面的函数模板表示的是右值引用的话,肯定是不能传递左值的,但是事实却是可以。这里的&&是一个未定义的引用类型,称为universal references,它必须被初始化,它是左值引用还是右值引用却决于它的初始化,如果它被一个左值初始化,它就是一个左值引用;如果被一个右值初始化,它就是一个右值引用。
注意:只有当发生自动类型推断时(如函数模板的类型自动推导,或auto关键字),&&才是一个universal references。
template<typename T>void f( T&& param); //这里T的类型需要推导,所以&&是一个 universal referencestemplate<typename T>class Test {Test(Test&& rhs); //Test是一个特定的类型,不需要类型推导,所以&&表示右值引用};void f(Test&& param); //右值引用//复杂一点template<typename T>void f(std::vector<T>&& param); //在调用这个函数之前,这个vector<T>中的推断类型//已经确定了,所以调用f函数的时候没有类型推断了,所以是 右值引用template<typename T>void f(const T&& param); //右值引用// universal references仅仅发生在 T&& 下面,任何一点附加条件都会使之失效
完美转发
c++中提供了一个std::forward()模板函数解决完美转发问题。
void myforward(int&& i){cout << "myforward(int&&):" << i << endl;process(std::forward<int>(i));}myforward(2); // process(int&&):2
上述函数的问题是不能转发左值。
解决办法就是借助universal references通用引用类型和std::forward()模板函数共同实现完美转发。
#include <iostream>#include <cstring>#include <vector>using namespace std;void RunCode(int &&m) {cout << "rvalue ref" << endl;}void RunCode(int &m) {cout << "lvalue ref" << endl;}void RunCode(const int &&m) {cout << "const rvalue ref" << endl;}void RunCode(const int &m) {cout << "const lvalue ref" << endl;}// 这里利用了universal references,如果写T&,就不支持传入右值,而写T&&,既能支持左值,又能支持右值template<typename T>void perfectForward(T && t) {RunCode(forward<T> (t));}template<typename T>void notPerfectForward(T && t) {RunCode(t);}int main(){int a = 0;int b = 0;const int c = 0;const int d = 0;notPerfectForward(a); // lvalue refnotPerfectForward(move(b)); // lvalue refnotPerfectForward(c); // const lvalue refnotPerfectForward(move(d)); // const lvalue refcout << endl;perfectForward(a); // lvalue refperfectForward(move(b)); // rvalue refperfectForward(c); // const lvalue refperfectForward(move(d)); // const rvalue ref}
深入理解
理解std::move()
右值的目的是指明对象是可移的。
形参都是左值,即使它是一个指向T的右值引用,因为它们可以在栈中取地址。
std::move()只做强制类型转换,返回参数的右值引用,不做任何其他动作。
右值允许被绑定到左值。不要对常量使用std::move()。将值移出对象会改变对象,所以语言不应该允许将常量传递到有可能改变它们的函数(如移动构造函数)。
class Base {};void f(const Base& b) {cout << "call f(const Base& b)" << endl;}void f(Base&& b) {cout << "call f(Base&& b)" << endl;}int main(){const Base b;f(move(b)); // 传入的类型为const Base&&,不匹配Base&&,但是匹配const Base&return 0;}// call f(const Base& b)
理解std::move()与std::forward()的区别
std::move()无条件地向右值型别转换,而std::forward()仅仅当被传入的实参被绑定到右值时,才对该实参进行向右值的型别转换。在运行期,二者都不会有任何的动作。
区分右值引用与万能引用
万能引用的现身场景:
- 模板函数的形参:
template
void f(T&& param);
- auto关键字
auto&& var2 = var1;
当&&不涉及型别推导时,那它一定是右值引用。
只有&&前的那个T被推导时才是万能引用。
template<typename T>void f(vector<T>&& param); // 不是万能引用int main(){vector<Base> v;f(v); //报错return 0;}
const的存在也会让万能引用失效。
template<typename T>void f(const T&& param); //不是万能引用
将std::move()应用于右值引用,而std::forward()应用于万能引用。
class Widget {public:Widget(Widget&& w) :name(move(w.name)),p_data(move(w.p_data)) {// 对右值引用使用move}template<typename T>void setName(T&& param) {name = forward<T>(param);//对万能引用使用forward}private:string name;shared_ptr<Base> p_data;};
知晓RVO(Return Value Optimization)
对于以值传递返回局部变量的函数,编译器会自动优化避免返回值的复制操作,即使不去避免复制,也会将其强制转换为右值,以进行移动复制操作,因此不用对返回值使用std::move()。
Widget f() {Widget w;return w; // 编译器会优化,不会产生复制,即使不优化,也会隐式地转换为下面的形式}Widget f() {Widget w;return move(w);}
万能引用的重载问题
对万能引用函数进行重载,几乎总会带来类型匹配问题。因为万能引用模板具化时生成的是精确的类型匹配,当遇到重载函数需要隐式类型转换时(包括添加const),模板具化的函数会优于重载函数。
避免方法可以看《Effective Modern C++》条款27,里面有一个很妙的手法。
引用折叠
在c++中不允许写引用的引用,即auto& &rx = w;非法,但是编译器在编译时会零时产生引用的引用,之后又会产生引用折叠,共四种情况(左-左,左-右-右-左,右-右),只有右-右时会折叠为右值引用,其余情况都是左值引用。
template <typename _Ty>constexpr _Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept { // forward an lvalue as either an lvalue or an rvaluereturn static_cast<_Ty&&>(_Arg);}
forward的实现就是利用了引用折叠。
constexpr保证在编译期完成转发,
remove_reference_t<_Ty>& _Arg保证其接受的参数为左值(完美转发时实参为左值)
当_Tp为左值类型(Widget&)时,_Tp&&会被折叠为左值类型,
当_Tp为右值类型(Widget&&)时,_Tp&&会被折叠为右值类型。
除了模板实例化和auto型别推导,typedef和decltype也会产生引用折叠。
template< typename T>void f(T&&) {typedef T&& Rvalue_of_T;}// 当传入的T为左值型别时(Widget&),Rvalue_of_T是Widget& &&,会被折叠为左值引用Widget&
完美引用失效
- 大括号初始化物
- 只有声明的static const成员变量
- 模板或重载的函数名
