C++ 11 左值,右值,左值引用,右值引用,std::move, std::foward
這篇文章要介紹的內容和標題一致,關於C++ 11中的這幾個特性網上介紹的文章很多,看了一些之後想把幾個比較關鍵的點總結記錄一下,文章比較長。給出了很多程式碼示例,都是編譯執行測試過的,希望能用這些幫助理解C++ 11中這些比較重要的特性。
關於左值和右值的定義
左值和右值在C中就存在,不過存在感不高,在C++尤其是C++11中這兩個概念比較重要,左值就是有名字的變數(物件),可以被賦值,可以在多條語句中使用,而右值呢,就是臨時變數(物件),沒有名字,只能在一條語句中出現,不能被賦值。
在 C++11 之前,右值是不能被引用的,最大限度就是用常量引用繫結一個右值,如 :
const int& i = 3;
在這種情況下,右值不能被修改的。但是實際上右值是可以被修改的,如 :
T().set().get();
T 是一個類,set 是一個函式為 T 中的一個變數賦值,get 用來取出這個變數的值。在這句中,T() 生成一個臨時物件,就是右值,set() 修改了變數的值,也就修改了這個右值。
既然右值可以被修改,那麼就可以實現右值引用。右值引用能夠方便地解決實際工程中的問題,實現非常有吸引力的解決方案。
右值引用
左值的宣告符號為”&”, 為了和左值區分,右值的宣告符號為”&&”。
給出一個例項程式如下
#include <iostream>
void process_value(int& i)
{
std::cout << "LValue processed: " << i << std::endl;
}
void process_value(int&& i)
{
std::cout << "RValue processed: " << i << std::endl;
}
int main()
{
int a = 0;
process_value(a);
process_value(1);
}
結果如下
wxl@dev:~$ g++ -std=c++11 test.cpp
wxl@dev:~$ ./a.out
LValue processed: 0
RValue processed: 1
Process_value 函式被過載,分別接受左值和右值。由輸出結果可以看出,臨時物件是作為右值處理的。
下面涉及到一個問題:
x的型別是右值引用,指向一個右值,但x本身是左值還是右值呢?C++11對此做出了區分:
Things that are declared as rvalue reference can be lvalues or rvalues. The distinguishing criterion is: if it has a name, then it is an lvalue. Otherwise, it is an rvalue.
對上面的程式稍作修改就可以印證這個說法
#include <iostream>
void process_value(int& i)
{
std::cout << "LValue processed: " << i << std::endl;
}
void process_value(int&& i)
{
std::cout << "RValue processed: " << std::endl;
}
int main()
{
int a = 0;
process_value(a);
int&& x = 3;
process_value(x);
}
wxl@dev:~$ g++ -std=c++11 test.cpp
wxl@dev:~$ ./a.out
LValue processed: 0
LValue processed: 3
x 是一個右值引用,指向一個右值3,但是由於x是有名字的,所以x在這裡被視為一個左值,所以在函式過載的時候選擇為第一個函式。
右值引用的意義
直觀意義:為臨時變數續命,也就是為右值續命,因為右值在表示式結束後就消亡了,如果想繼續使用右值,那就會動用昂貴的拷貝建構函式。(關於這部分,推薦一本書《深入理解C++11》)
右值引用是用來支援轉移語義的。轉移語義可以將資源 ( 堆,系統物件等 ) 從一個物件轉移到另一個物件,這樣能夠減少不必要的臨時物件的建立、拷貝以及銷燬,能夠大幅度提高 C++ 應用程式的效能。臨時物件的維護 ( 建立和銷燬 ) 對效能有嚴重影響。
轉移語義是和拷貝語義相對的,可以類比檔案的剪下與拷貝,當我們將檔案從一個目錄拷貝到另一個目錄時,速度比剪下慢很多。
通過轉移語義,臨時物件中的資源能夠轉移其它的物件裡。
在現有的 C++ 機制中,我們可以定義拷貝建構函式和賦值函式。要實現轉移語義,需要定義轉移建構函式,還可以定義轉移賦值操作符。對於右值的拷貝和賦值會呼叫轉移建構函式和轉移賦值操作符。如果轉移建構函式和轉移拷貝操作符沒有定義,那麼就遵循現有的機制,拷貝建構函式和賦值操作符會被呼叫。
普通的函式和操作符也可以利用右值引用操作符實現轉移語義。
轉移語義以及轉移建構函式和轉移複製運算子
以一個簡單的 string 類為示例,實現拷貝建構函式和拷貝賦值操作符。
class MyString {
private:
char* _data;
size_t _len;
void _init_data(const char *s) {
_data = new char[_len+1];
memcpy(_data, s, _len);
_data[_len] = '\0';
}
public:
MyString() {
_data = NULL;
_len = 0;
}
MyString(const char* p) {
_len = strlen (p);
_init_data(p);
}
MyString(const MyString& str) {
_len = str._len;
_init_data(str._data);
std::cout << "Copy Constructor is called! source: " << str._data << std::endl;
}
MyString& operator=(const MyString& str) {
if (this != &str) {
_len = str._len;
_init_data(str._data);
}
std::cout << "Copy Assignment is called! source: " << str._data << std::endl;
return *this;
}
virtual ~MyString() {
if (_data) free(_data);
}
};
int main() {
MyString a;
a = 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._data << std::endl;
_len = str._len;
_data = str._data;
str._len = 0;
str._data = NULL;
}
有下面幾點需要對照程式碼注意:
1. 引數(右值)的符號必須是右值引用符號,即“&&”。
2. 引數(右值)不可以是常量,因為我們需要修改右值。
3. 引數(右值)的資源連結和標記必須修改。否則,右值的解構函式就會釋放資源。轉移到新物件的資源也就無效了。
現在我們定義轉移賦值操作符。
MyString& operator=(MyString&& str) {
std::cout << "Move Assignment is called! source: " << str._data << std::endl;
if (this != &str) {
_len = str._len;
_data = str._data;
str._len = 0;
str._data = NULL;
}
return *this;
}
這裡需要注意的問題和轉移建構函式是一樣的。
增加了轉移建構函式和轉移複製操作符後,我們的程式執行結果為 :
由此看出,編譯器區分了左值和右值,對右值呼叫了轉移建構函式和轉移賦值操作符。節省了資源,提高了程式執行的效率。
有了右值引用和轉移語義,我們在設計和實現類時,對於需要動態申請大量資源的類,應該設計轉移建構函式和轉移賦值函式,以提高應用程式的效率。
關於std::move()和std::forward 再次推薦一本書:《effective modern C++》
英文版的,這裡有篇關於其中item25的翻譯不錯
但是這幾點總結的不錯
std::move執行一個無條件的轉化到右值。它本身並不移動任何東西;
std::forward把其引數轉換為右值,僅僅在那個引數被繫結到一個右值時;
std::move和std::forward在執行時(runtime)都不做任何事。