参考文章:ios
刷 Leetcode 时,时不时遇到以下 2 种遍历 STL 容器的写法:git
int main() { vector<int> v = {1, 2, 3, 4}; for (auto &x: v) cout<<x<<' '; cout<<endl; for (auto &&x: v) cout<<x<<' '; cout<<endl; }
一个困扰我好久的问题是 auto &
和 auto &&
有什么区别?github
首先要明确一个概念,值 (Value) 和变量 (Variable) 并非同一个东西:ide
i + j + k
)。左值(lvalue, left value),顾名思义就是赋值符号左边的值。准确来讲, 左值是表达式(不必定是赋值表达式)后依然存在的持久对象。函数
右值(rvalue, right value),右边的值,是指表达式结束后就再也不存在的临时对象。性能
C++11 中为了引入强大的右值引用,将右值的概念进行了进一步的划分,分为:纯右值和将亡值。优化
纯右值 (prvalue, pure rvalue),纯粹的右值,要么是纯粹的字面量,例如
10, true
; 要么是求值结果至关于字面量或匿名临时对象,例如1+2
。非引用返回的临时变量、运算表达式产生的临时变量、原始字面量、Lambda 表达式都属于纯右值。this
C++( 包括 C ) 中全部的表达式和变量要么是左值,要么是右值。通俗的左值的定义就是非临时对象,那些能够在多条语句中使用的对象。全部的变量都知足这个定义,在多条代码中均可以使用,都是左值。右值是指临时的对象,它们只在当前的语句中有效。spa
例子:翻译
int i = 0; // ok, i is lvalue, 0 is rval // 右值也能够出如今赋值表达式的左边, 可是不能做为赋值的对象,由于右值只在当前语句有效,赋值没有意义。 // 0 做为右值出如今了”=”的左边。可是赋值对象是 i 或者 j,都是左值。 (i > 0? i : j) = 233
总结:
须要注意的是,字符串字面量只有在类中才是右值,当其位于普通函数中是左值。例如:
class Foo { const char *&&right = "this is a rvalue"; // 此处字符串字面量为右值 // const char *&right = "hello world"; // error public: void bar() { right = "still rvalue"; // 此处字符串字面量为右值 } }; int main() { const char *const &left = "this is an lvalue"; // 此处字符串字面量为左值 // left = "123"; // error }
将亡值 (xvalue, expiring value),是 C++11 为了引入右值引用而提出的概念 (所以在传统 C++ 中,纯右值和右值是同一个概念),也就是即将被销毁、却可以被移动的值。将亡值表达式,即:
move
先看一个例子:
vector<int> foo() { vector<int> v = {1,2,3,4,5}; return v; } auto v1 = foo();
按照传统 C++ 的方式(也是咱们这些 C++ 菜鸟的理解),上述代码的执行方式为:foo()
在函数内部建立并返回一个临时对象 v
,而后执行 vector<int>
的拷贝构造函数,完成 v1
的初始化,最后对 foo
内的临时对象进行销毁。
那么,在某一时刻,就存在 2 份相同的 vector
数据。若是这个对象很大,就会形成大量额外的开销。
在 v1 = foo()
中,v1
是一个左值,能够被继续使用,但foo()
就是一个纯右值, foo()
产生的那个返回值做为一个临时值,一 旦被 v1
复制后,将当即被销毁,没法获取、也不能修改。
而将亡值就定义了这样一种行为: 临时的值可以被识别、同时又可以被移动。
在 C++11 以后,编译器为咱们作了一些工做,foo()
内部的左值 v
会被进行隐式右值转换,等价于 static_cast<vector<int> &&>(v)
,进而此处的 v1
会将 foo
局部返回的值进行移动。也就是后面将会提到的移动语义 std::move()
。
我的的理解是,这种语法的引入是为了实现与 Java 中相似的对象引用系统。
先看一段代码:
int a; a = 2; //a是左值,2是右值 a = 3; //左值能够被更改,编译经过 2 = 3; //右值不能被更改,错误 int b = 3; int* pb = &b; //pb是左值,&b是右值,由于它是由取址运算符返回的值 &b = 0; //错误,右值不能被更改 // lvalues: int i = 42; i = 43; // ok, i is an lvalue int* p = &i; // ok, i is an lvalue int& foo(); foo() = 42; // ok, foo() is an lvalue int* p1 = &foo(); // ok, foo() is an lvalue // rvalues: int foobar(); int j = 0; j = foobar(); // ok, foobar() is an rvalue int k = j + 2; // ok, j+2 is an rvalue int* p2 = &foobar(); // error, cannot take the address of an rvalue j = 42; // ok, 42 is an rvalue
那么问题来了:函数返回值是否只会是右值?固然不是。
vector<int> v(10, 0); v[0] = 111;
显然,v[0]
会执行 []
的符号重载函数 int& operator[](const int x)
, 所以函数的返回值也是可能为左值的。
要拿到一个将亡值,就须要用到右值引用 T &&
,其中 T
是类型。右值引用的声明让这个临时值的生命周期得以延长,只要变量还活着,那么将亡值将继续存活。
C++11 提供了 std::move
这个方法将左值参数无条件的转换为右值,有了它咱们就可以方便的得到一个右值临时对象,例如:
#include <iostream> #include <string> using namespace std; void reference(string &str) { cout << "lvalue ref" << endl; } void reference(string &&str) { cout << "rvalue ref" << endl; } int main() { string lv1 = "string,"; // lv1 is lvalue // string &&r1 = lv1; // 非法,右值引用不能引用左值 string &&rv1 = std::move(lv1); // 合法,move 可将左值转移为右值 cout << rv1 << endl; // string &lv2 = lv1 + lv1; // 非法,很是量引用的初始值必须为左值 const string &lv2 = lv1 + lv1; // 合法,常量左值引用可以延长临时变量的生命周期 cout << lv2 << endl; string &&rv2 = lv1 + lv2; // 合法,右值引用延长临时对象生命周期(经过 rvalue reference 引用 rval) rv2 += "Test"; cout << rv2 << endl; reference(rv2); // 输出 "lvalue ref" // rv2 虽然引用了一个右值,但因为它是一个引用,因此 rv2 依然是一个左值。 // 也就是说,T&& Doesn’t Always Mean “Rvalue Reference”, 它既能够绑定左值,也能绑定右值 }
为何不容许很是量引用绑定到左值?
一种解释以下(C++ 真傻逼)。
这个问题至关于解释下面一段代码:
int i = 233; int &r0 = i; // ok double &r1 = i; // error const double &r3 = i; // ok
由于 double &r1
类型与 int i
不匹配,因此不行,那为何 const double &r3 = i
是能够的?由于它实际上至关于:
const double t = (double)i; const double &r3 = t;
在 C++ 中,全部的临时变量都是 const
类型的,因此没有 const
就不行。
先看一段代码,熟悉一下 move
作了些什么:
#include <iostream> #include <string> using namespace std; int main() { string a = "sinkinben"; string b = move(a); cout << "a = \"" << a << "\"" << endl; cout << "b = \"" << b << "\"" << endl; } // Output // a = "" // b = "sinkinben"
而后看完下面一段代码,结束这一回合。
template <class T> swap(T& a, T& b){ T tmp(a); //现有两份a的拷贝,tmp和a a = b; //现有两份b的拷贝,a和b b = tmp; //现有两份tmp的拷贝,b和tmp } //试试更好的方法,不会生成额外的拷贝 template <class T> swap(T& a, T& b){ T tmp(std::move(a)); //只有一份拷贝,tmp a = std::move(b); //只有一份拷贝,a b = std::move(tmp); //只有一份拷贝,b }
我的感受,b = move(a)
这一语义操做,是把变量 b
绑定到数据 a
的内存区域上,从而避免了无心义的数据拷贝操做。
下面这一段代码能够印证个人这个观点。
#include <iostream> class A { public: int *pointer; A() : pointer(new int(1)) { std::cout << "构造" << pointer << std::endl; } A(A &a) : pointer(new int(*a.pointer)) { std::cout << "拷贝" << pointer << std::endl; } // 无心义的对象拷贝 A(A &&a) : pointer(a.pointer) { a.pointer = nullptr; std::cout << "移动" << pointer << std::endl; } ~A() { std::cout << "析构" << pointer << std::endl; delete pointer; } }; // 防止编译器优化 A return_rvalue(bool test) { A a, b; if (test) return a; // 等价于 static_cast<A&&>(a); else return b; // 等价于 static_cast<A&&>(b); } int main() { A obj = return_rvalue(false); std::cout << "obj:" << std::endl; std::cout << obj.pointer << std::endl; std::cout << *obj.pointer << std::endl; return 0; } /* Output 构造0x7f8477405800 构造0x7f8477405810 移动0x7f8477405810 析构0x0 析构0x7f8477405800 obj: 0x7f8477405810 1 析构0x7f8477405810 */
对于 queue
或者 vector
,咱们也能够经过 move
提升性能:
// q is a queue auto x = std::move(q.front()); q.pop(); // v is a vertor v.push_back(std::move(x));
若是 STL 中的元素「体积」都很大,这么作也能节省一点开销,提升性能。
恕我直言,这个翻译是个辣鸡。英文名叫 Perfect Forwarding .
这是为了解决这样一个问题:实参被传入到函数中,当它被再传到另外一个函数中,它依然是一个左值或右值。
template <class T> void f2(T t){ cout<<"f2"<<endl; } template <class T> void f1(T t){ cout<<"f1"<<endl; f2(t); //若是t是右值,咱们但愿传入f2也是右值;若是t是左值,咱们但愿传入f2也是左值 } //在main函数里: int a = 2; f1(3); //传入右值 f1(a); //传入左值
在引进👆巴拉巴拉的这一套机制以前,即 C++11以前的状况是怎么样的呢?当咱们从 f1
调用 f2
的时候,无论传入 f1
的是右值仍是左值,由于 t
是一个变量名,传入 f2
的时候都变成了左值,这就会形成由于调用 T
的拷贝构造函数而生成没必要要的拷贝浪费大量资源。
那么如今有一个叫 forward
的函数,就能够这样作:
template <class T> void f2(T t){ cout<<"f2"<<endl; } template <class T> void f1(T&& t) { //这是通用引用,而不是右值引用 cout<"f1"<<endl; f2(std::forward<T>(t)); //std::forward<T>(t)用来把t转发为左值或右值,决定于T }
这样,f1
调用 f2
的时候,调用的就是移动构造函数而不是拷贝构造函数,能够避免没必要要的拷贝,这就叫「完美转发」。
完美转发,傻逼到家。
本文开始提出的问题 auto &
和 auto &&
有什么区别?这个问题就更复杂了,涉及到 Universal Reference 这个概念,能够参考这 2 篇文章:
有空再说。
傻逼 C++ 。