什麼是移動構造
在 C++ 11 標準之前(C++ 98/03 標準中),如果想用其它對象初始化一個同類的新對象,只能藉助類中的複製(拷貝)構造函數。在C++11中,引入了右值引用,提供了左值轉右值的方法,避免了對象潛在的拷貝。而移動構造函數和移動賦值運算符也是通過右值的屬性來實現的。直觀的來講,移動構造就是將對象的狀態或者所有權從一個對象轉移到另一個對象。只是轉移,沒有內存的搬遷或者內存拷貝所以可以提高利用效率,改善性能。
右值和左值
CPU視角的右值和左值
通過一個最簡答的程序來看一下CPU是如何看待左值和右值的。
void push(int && x){
int y = x;
}
void push(int & x){
int y = x;
}
上面程序對應的彙編代碼為:
push(int&&):
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-24], rdi
mov rax, QWORD PTR [rbp-24]
mov eax, DWORD PTR [rax]
mov DWORD PTR [rbp-4], eax
nop
pop rbp
ret
push(int&):
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-24], rdi
mov rax, QWORD PTR [rbp-24]
mov eax, DWORD PTR [rax]
mov DWORD PTR [rbp-4], eax
nop
pop rbp
ret
可以看到彙編指令對於右值和左值的處理是完全相同的,因此無論是左值和右值在CPU看來都是完全相同的。
語言層面的概念
對於底層CPU來説,左值和右值是完全無感的,但是對於C++語言來説左值和右值是非常重要的概念。淺顯的來看,對於左值來説,編譯器是允許寫操作的;然而對於右值來説,只允許讀操作。左值是有完整的生命週期的,而右值往往在執行完需要它的代碼就直接被銷燬了(如x=1;中的1)。
早期的C++左值就是左值,右值就是右值,不可改變。來到了C++11,語言為程序員提供了將左值轉換為右值的方法---std::move。
std::move並不能移動任何東西,它唯一的功能是將一個左值強制轉化為右值引用,繼而可以通過右值引用使用該值,以用於移動語義。從實現上講,std::move基本等同於一個類型轉換。右值在完成期任務的時候會立刻被析構,因此在使用移動語義時需要防止產生空指針的問題。
移動構造函數和移動構造賦值函數
以一個例子來理解移動構造函數和移動構造賦值函數
class Item{
public:
int* x;
Item()=default;
Item(int val){ x = new int(val);};
Item(const Item& item){
x = new int(*item.x);
printf("copy\n");
};
Item(Item&& item){
x = item.x;
item.x = NULL;
printf("move\n");
};
Item& operator=(const Item& item){
if(this != &item){
this->x = new int(*item.x);
}
printf("copy=\n");
return *this;
}
Item& operator=(Item&& item){
if(this != &item){
this->x = item.x;
item.x = NULL;
}
printf("move=\n");
return *this;
}
~Item(){
delete x;
};
};
我們首先看一下移動構造函數和普通複製構造函數的區別:
Item(const Item& item){
x = new int(*item.x);
printf("copy\n");
};
Item(Item&& item){
x = item.x;
item.x = NULL;
printf("move\n");
};
可以發現有以下幾點不同:
- 移動構造函數沒有新申請成員變量內存,而是直接拿來了輸入成員變量指向的內存。
- 移動構造函數對輸入(右值)的x指針賦值為NULL,因為右值在執行完之後會被析構,x指向的內存會被釋放,因此安全的做法是將右值中的x指向NULL(析構時不會產生任何內存釋放)。
- 移動構造函數輸入不能是const變量。因為需要修改成員變量x指向NULL。
接下來觀察以下複製賦值運算符和移動賦值運算符的區別:
Item& operator=(const Item& item){
this->x = new int(*item.x);
printf("copy=\n");
return *this;
}
Item& operator=(Item&& item){
if(this != &item){
this->x = item.x;
item.x = NULL;
}
printf("move=\n");
return *this;
}
這兩種運算符類比於對應的構造構造函數原理基本相同,需要注意賦值運算符是單目運算符,調用方是等號左側的對象。因此可以用this指針來對左側元素進行修改。
需要注意的是:移動賦值運算符多了一步判斷:判斷當前指針與輸入的右值地址是否相同。
這是為了防止把自身作為輸入進行移動賦值,這樣會導致得到的對象成員x指向NULL。
總結
移動構造提升了賦值和初始化的性能,看似與複製構造只有輸入上的不同,但是內部實現充滿了種種細節,在編寫代碼的時候一定要留心,防止在拷貝過程中出現內部指針對象為空的情況。