- C左值右值
- 左值和右值的由來
- 什么是左值和右值
- 左值右值的本質
- 引用
- 左值引用
- 右值引用
- 移動語句與完美轉發
- 移動語句
- 實現移動構造函數和轉移賦值函數
- stdmove
- 完美轉發Perfect Forwarding
- 移動語句
C++左值右值
自從C++11發布之后,出現了一個新的概念,即左值和右值,英文為lvalue和rvalue,這兩個是比較晦澀難懂的基礎概念,為什么說是基礎的概念呢?因為只有了解了它,才能真正理解move和forward語句。
左值和右值的由來
左值和右值,最早是從C語言繼承而來的。在C語言中,或者是它的繼承版本中有如下變現形式:
- 左值是可以位于賦值運算符“=”左側的變量或表達式,也可以位于賦值運算符“=”右側
- 右值是不可以位于賦值運算符“=”左側的表達式,只能出現在等號右邊的變量或者表達式
我們來看看例子:
例子1:
int a; //聲明變量a
int b; //聲明變量ba = 3; //賦值語句,將a的值重新賦值為3,此時a為左值
b = 4; //此時b為左值a = b; //此時a為左值,b為右值
b = a;//此時b為左值,a為右值3 = a; //編譯錯誤,這里應該很好理解,3不可能再被賦值了
a + b = 4; //編譯錯誤,這里相當于7 = 4,也是不合理的
例子2
//定義了兩個函數foo1和foo2
int foo1(int number)
{return number;
}int foo2( int number )
{return number;
}main()
{foo1(1) = foo2(2); //編譯錯誤,因為foo1和foo2的返回值只能作為右值,不能放在等號的左邊int temp = foo1(1) * foo2(2); //temp為左值,foo1(1) * foo2(2)為右值
}
上面這些例子就是左值和右值的例子。
什么是左值和右值
一個變量或者表達式是左值還是右值,取決于我們使用的是它的值還是它在內存中的位置(作為實例的身份)。
int a; //聲明變量a
int b; //聲明變量ba = b; //此時a為左值,b為右值
這個例子中,將b的值賦值給a,將值保存在a的內存中,b在這里面是右值,a在這里面是左值
因為b作為實例既可以當做左值也可以當做右值。
所以判斷一個值是左值還是右值要根據實際在語句匯總的含義來確定。
總結第一點:
- 在一般情況下,需要右值的地方可以用左值來代替,需要左值的地方必須使用左值
- 左值存放在實例中,有持久的狀態,而右值是字面常量,要么是在表達式求值過程中創建的臨時實例,沒有持久的狀態
重點:
能取得到地址的變量或者表達式就是左值,反之為右值。
那現在我們來看看下面的例子哪個是左值,哪個是右值?
int a = 0;
int b = 1;
a++;
++b;
我來宣布答案,a++為右值,++b為左值,首先我們先驗證一下:
int a = 0;
int b = 1;
a++ = 5; //error: lvalue required as left operand of assignment
++b = 5;
實驗的結果也是正確的,那我們來分析一下:
對于a++
1. a++首先產生一個臨時變量,記錄a的值
2. 然后將a+1
3. 接著返回臨時變量
根據這個過程我們知道 int a = 0;int c = a++; 的值應該是c為0;而a變為了1,
所以a++此時將臨時變量返回給了c,那么這個臨時變量我們是不能獲取地址的,也就
是使用“&”。所以結論就是a++為右值。
對于++b
1. 進行了b = b + 1
2. 返回變量b
根據這個過程,我們是可以取b的地址的,所以b是左值。
左值右值的本質
int a = 5;
int c = a + a;
a就是左值,5就是右值。 a + a 表達式中,a以右值傳入,相加之后也以右值返回。
左值就是對一塊內存區域的引用(這個并不是c++11中的int &a 之類的引用),
比如上邊的a,就對應了一塊內存區域(起始地址&a,大小為sizeof(int) )。
專業的解釋:
An object is a region of storage that can be examined and stored into. An lvalue is an expression that refers to such an object. An lvalue does not necessarily permit modification of the object it designates. For example, a const object is an lvalue that cannot be modified.
對于每個變量,都有2個值與其相關聯:
- 數據值,存儲在某個內存地址中,也稱為右值,右值是被讀取的值,不可修改。
- 地址值,即存儲數據值的那塊內存地址,也稱左值。
所以左值既可以當作左值也可以作為右值。就是這么神奇。
引用
在C++中,有兩種對實例的引用:左值引用和右值引用。
左值引用
左值引用是常見的引用,C++中可以使用“&”符號定義引用,如果一個左值同時也是引用,那么就稱其為“左值引用”。如:
std::string str;
std::string& strRef = str; // strRef為左值也為引用,稱其為左值引用
非const左值引用不能使用右值對其賦值
std::string& strRef = "abc"; // error: abc字符串為右值,
假設上面可以的話,就會遇到一個問題:如何修改右值的值?因為引用是可以后續被賦值的。根據上面的定義,右值連可被獲取的內存地址都沒有,也就談不上對其進行賦值。
但是const左值引用就可以使用右值,因為常量不能被修改,也不存在上面糾結的問題:
const std::string strRef = "abc";
再比如,我們經常使用左值作為函數的參數類型,可以減少不必要的對象復制:
int foo( int& number )
{return number;
}main()
{int a = foo(1); //錯誤int b = 1;int c = foo(b); //通過
}
我們將上面的int& number改為 const int& number即可。
補充知識:
什么是CV限定符(CV-qualified),如果變量聲明時類型帶有const或者volatile,就說此變量類型具有CV限定符。
右值引用
右值引用也是引用,但是它只能且必須綁定在右值上。
int a = 5;
int& b = a; // a綁定在左值引用b上
int&& c = a; // error:a可以是左值,所以不能將它綁定在右值引用上。
int&& d = 30; // 將右值30綁定在右值引用上
int&& e = a * 1 // a * 1的結果是一個臨時對象,為右值,所以可以綁定在右值引用上
結論:
由于右值引用只能綁定在右值上,而右值要么是字面常量,要么是臨時對象,所以:
右值引用的對象,是臨時的,即將被銷毀; 并且右值引用的對象,不會在其它地方使用。
移動語句與完美轉發
這里涉及前面提過的move和forward兩個函數,即移動語句和轉發。
右值引用 (Rvalue Referene) 是 C++ 新標準 (C++11, 11 代表 2011 年 ) 中引入的新特性 , 它實現了轉移語義 (Move Sementics) 和精確傳遞 (Perfect Forwarding)。它的主要目的有兩個方面:
- 消除兩個對象交互時不必要的對象拷貝,節省運算存儲資源,提高效率。
- 能夠更簡潔明確地定義泛型函數。
移動語句
右值引用是用來支持移動語句的。移動語句可以將資源(堆,系統對象等)從一個對象轉移到另一個對象,這樣能夠減少不必要的臨時對象的創建、拷貝以及銷毀,能夠大幅度提高C++應用程序的性能。臨時對象的維護(創建和銷毀)對性能有嚴重影響。
移動語句是和拷貝語句相對的,可以類比文件的剪切和拷貝,當我們將文件從一個目錄拷貝到另一個目錄時,速度比剪切慢很多。
通過移動語句,臨時對象中的資源能夠轉移其他的對象里。
在現有的 C++ 機制中,我們可以定義拷貝構造函數和賦值函數。要實現移動語句,需要定義移動構造函數,還可以定義移動賦值操作符。對于右值的拷貝和賦值會調用移動構造函數和移動賦值操作符。如果移動構造函數和移動拷貝操作符沒有定義,那么就遵循現有的機制,拷貝構造函數和賦值操作符會被調用。
實現移動構造函數和轉移賦值函數
class MyString { public: MyString() { m_data = nullptr; m_len = 0; }private: char* m_data; size_t m_len; void initData(const char *s) { m_data = new char[m_len+1]; memcpy(m_data, s, m_len); m_data[m_len] = '\0'; } MyString(const char* p) { m_len = strlen (p); initData(p); } MyString(const MyString& str) { m_len = str.m_len; initData(str.m_data); std::cout << "Copy Constructor is called! source: " << str.m_data << std::endl; } MyString& operator=(const MyString& str) { if (this != &str) { m_len = str.m_len; initData(str.m_data); } std::cout << "Copy Assignment is called! source: " << str.m_data << std::endl; return *this; } virtual ~MyString() { } }; int main()
{ MyString a; a = MyString("Hello"); //調用拷貝構造函數,MyString("Hello")為臨時對象,即右值std::vector<MyString> vec; vec.push_back(MyString("World"));
}運行結果:
Copy Assignment is called! source: Hello
Copy Constructor is called! source: World
這個 string 類已經基本滿足我們演示的需要。在 main函數中,實現了調用拷貝構造函數的操作和拷貝賦值操作符的操作。
MyString(“Hello”) 和 MyString(“World”)都是臨時對象,也就是右值。雖然它們是臨時的,但程序仍然調用了拷貝構造和拷貝賦值,造成了沒有意義的資源申請和釋放的操作。如果能夠直接使用臨時對象已經申請的資源,既能節省資源,有能節省資源申請和釋放的時間。這正是定義轉移語義的目的。
MyString(MyString&& str)
{ std::cout << "Move Constructor is called! source: " << str.m_data << std::endl; m_len = str.m_len; m_data = str.m_data; str.m_len = 0; str.m_data = nullptr;
}MyString& operator=(MyString&& str)
{ std::cout << "Move Assignment is called! source: " << str.m_data << std::endl; if (this != &str) { m_len = str.m_len; m_data = str.m_data; str.m_len = 0; str.m_data = nullptr; } return *this;
}增加后的運行結果:Move Assignment is called! source: Hello
Move Constructor is called! source: World
由此看出,編譯器區分了左值和右值,對右值調用了移動構造函數和移動賦值操作符。節省了資源,提高了程序運行的效率。
有了右值引用和移動語義,我們在設計和實現類時,對于需要動態申請大量資源的類,應該設計移動構造函數和轉移賦值函數,以提高應用程序的效率。
std::move
我們先來看一個例子:
void processValue( int& value )
{std::cout << "lvalue process: " << value << std::endl;
}void processValue( int&& value )
{std::cout << "rvalue process: " << value << std::endl;
}main()
{int a = 0;processValue(a); //傳入左值processValue(1); //傳如右值
}結果:
lvalue process: 0
rvalue process: 1
通過這個例子來看,processValue函數被重載,分別接受左值和右值。由輸出結果可以看出,臨時對象是作為右值處理的。
但是如果臨時對象通過一個接受右值的函數傳遞給另一個函數時,就會變成左值,因為這個臨時對象在傳遞過程中,變成了命名對象。
void processValue( int& value )
{std::cout << "lvalue process: " << value << std::endl;
}void processValue( int&& value )
{std::cout << "rvalue process: " << value << std::endl;
}void forwardValue( int&& value )
{processValue(value);
}main()
{int a = 0;processValue(a);processValue(1);forwardValue(2);
}結果:
lvalue process: 0
rvalue process: 1
lvalue process: 2
我們可以看出最后一個函數調用,2是右值,可以返回的時候卻變成了左值。這里面我們可以使用std::move(var)將變量轉移為右值語句。
修改為:
...
void forwardValue( int&& value )
{processValue(std::move(value) );
}
...
既然編譯器只對右值引用才能調用轉移構造函數和轉移賦值函數,而所有命名對象都只能是左值引用,如果已知一個命名對象不再被使用而想對它調用轉移構造函數和轉移賦值函數,也就是把一個左值引用當做右值引用來使用,怎么做呢?標準庫提供了函數 std::move,這個函數以非常簡單的方式將左值引用轉換為右值引用。
std::move在提高 swap 函數的的性能上非常有幫助,一般來說,swap函數的通用定義如下:
template <class T> swap(T& a, T& b)
{ T tmp(a); // copy a to tmp a = b; // copy b to a b = tmp; // copy tmp to b
}
有了 std::move,swap 函數的定義變為 :
template <class T> swap(T& a, T& b)
{ T tmp(std::move(a)); // move a to tmp a = std::move(b); // move b to a b = std::move(tmp); // move tmp to b
}
通過 std::move,一個簡單的 swap 函數就避免了 3 次不必要的拷貝操作。
完美轉發(Perfect Forwarding)
Perfect Forwarding也被翻譯成完美轉發,精準轉發等,說的都是一個意思。
用于這樣的場景:需要將一組參數原封不動的傳遞給另一個函數。
原封不動”不僅僅是參數的值不變,在 C++ 中,除了參數值之外,還有一下兩組屬性:
- 左值/右值
- const/non-const。
完美轉發就是在參數傳遞過程中,所有這些屬性和參數值都不能改變。
重點:完美轉發僅與類模板或函數模板的環境有關。
示例:
class Person
{
public:template<typename T1, typename T2>Person(T1&& first, T2&& second ) : firstname{std::forward<T1>(first)},secondname{std::forward<T2>(second)}{}string getName() const{return firstname.getName() + " " + secondname.getName(); }
private:Name firstname;Name secondname;
}class Name
{
public:Name( const string& aName ): name{aName}{cout << "Lvalue Name constructor." << endl;}Name( string&& aName ): name{std::move(aName)}{cout << "Rvalue Name constructor." << endl;}const string& getName() const { return name; }
private:string name;
}main()
{Person me{string{"abc"}, string{"def"}};string first{"lll"};string second{"ggg"};Person other{first, second};
}輸出結果:
Rvalue Name constructor.
Rvalue Name constructor.
Lvalue Name constructor.
Lvalue Name constructor.