C++11 移動建構函式
文章目錄
一、引言
移動建構函式是什麼?先舉個例子,你有一本書,你不想看,但我很想看,那麼我有哪些方法可以讓我能看這本書?有兩種做法,一種是你直接把書交給我,另一種是我去買一些稿紙來,然後照著你這本書一字一句抄到稿紙上。
顯然,第二種方法很浪費時間,但這正是有些深拷貝建構函式的做法,而移動建構函式便能像第一種做法一樣省時,第一種做法在 C++ 中叫做完美轉發。
二、左值和右值
何為左值?能用取址符號 &
取出地址的皆為左值,剩下的都是右值。
而且,匿名變數一律屬於右值。
int i = 1; // i 是左值,1 是右值
int GetZero {
int zero = 0;
return zero;
}
//j 是左值,GetZero() 是右值,因為返回值存在於暫存器中
int j = GetZero();
//s 是左值,string("no name") 是匿名變數,是右值
string s = string("no name");
三、深拷貝建構函式
用 g++ 編譯器編譯下列程式碼時記得加上引數 -fno-elide-constructors
#include <iostream>
#include <string>
using namespace std;
class Integer {
public:
//引數為常量左值引用的深拷貝建構函式,不改變 source.ptr_ 的值
Integer(const Integer& source)
: ptr_(new int(*source.ptr_)) {
cout << "Call Integer(const Integer& source)" << endl;
}
//引數為左值引用的深拷貝建構函式,轉移堆記憶體資源所有權,改變 source.ptr_ 的值
Integer(Integer& source)
: ptr_(source.ptr_) {
source.ptr_ = nullptr;
cout << "Call Integer(Integer& source)" << endl;
}
Integer(int value)
: ptr_(new int(value)) {
cout << "Call Integer(int value)" << endl;
}
~Integer() {
cout << "Call ~Integer()" << endl;
delete ptr_;
}
int GetValue(void) { return *ptr_; }
private:
string name_;
int* ptr_;
};
int
main(int argc, char const* argv[]) {
Integer a(Integer(100));
int a_value = a.GetValue();
cout << a_value << endl;
cout << "-----------------" << endl;
Integer temp(10000);
Integer b(temp);
int b_value = b.GetValue();
cout << b_value << endl;
cout << "-----------------" << endl;
return 0;
}
執行結果如下。
Call Integer(int value)
Call Integer(const Integer& source)
Call ~Integer()
100
-----------------
Call Integer(int value)
Call Integer(Integer& source)
10000
-----------------
Call ~Integer()
Call ~Integer()
Call ~Integer()
在程式中,引數為常量左值引用的深拷貝建構函式的做法相當於引言中的第二種做法,“重買稿紙”相當於再申請一次堆記憶體資源,“重新抄寫”相當於把匿名物件 Integer(100)
的資源拷貝到物件 a
這邊;引數為左值引用的深拷貝建構函式的做法則相當於引言中的第一種做法,語句 ptr_(source.ptr_)
和 source.ptr_ = nullptr;
的作用相當於“我直接把書拿給你”。
由執行結果可以看出,當同時存在引數型別為常量左值引用和左值引用的深拷貝建構函式時,匿名物件 Integer(100)
只能選擇前者,非匿名物件 temp
可以選擇後者,這是因為常量左值引用可以接受左值、右值、常量左值、常量右值,而左值引用只能接受左值。因此,對於匿名變數,引數為任何型別左值引用的深拷貝建構函式都無法實現完美轉發。還有一種辦法——右值引用。
四、右值引用
右值引用也是引用的一種,引數型別為右值引用的函式只能接受右值引數,但不包括模板函式,引數型別為右值引用的模板函式不在本文討論的範圍內。
五、移動建構函式
移動建構函式是引數型別為右值引用的拷貝建構函式。
在“三”示例程式 Interger
類的定義中新增一個移動建構函式,其餘保持原樣。
//引數為左值引用的深拷貝建構函式,轉移堆記憶體資源所有權,改變 source.ptr_ 的值
Integer(Integer& source)
: ptr_(source.ptr_) {
source.ptr_ = nullptr;
cout << "Call Integer(Integer& source)" << endl;
}
//移動建構函式,與引數為左值引用的深拷貝建構函式基本一樣
Integer(Integer&& source)
: ptr_(source.ptr_) {
source.ptr_ = nullptr;
cout << "Call Integer(Integer&& source)" << endl;
}
Integer(int value)
: ptr_(new int(value)) {
cout << "Call Integer(int value)" << endl;
}
執行結果如下。
Call Integer(int value)
Call Integer(Integer&& source)
Call ~Integer()
100
-----------------
Call Integer(int value)
Call Integer(Integer& source)
10000
-----------------
Call ~Integer()
Call ~Integer()
Call ~Integer()
只有第二行跟先前不同,匿名物件 Integer(100)
也能通過移動建構函式實現完美轉發。
大家可能會有疑問,上文提及到常量左值引用也可以接受右值,而右值引用也可以接受右值,那一個右值是否有可能會套入一個引數型別為常量左值引用的函式呢?答案是不會,一個右值要套入函式時,會優先選擇套入引數型別為右值引用的函式。
可是仔細想想還是有點不滿意,如果要讓左值和右值的深拷貝都能實現完美轉發,就需要寫兩個內容基本一樣的拷貝建構函式,一個引數為(非常量)左值引用,一個引數為右值,那能不能只用一個函式就能實現左值、右值兩者的深拷貝完美轉發呢?答案就是強制型別轉換,將左值強制強制轉換為右值,再套入引數型別為右值引用的深拷貝建構函式。
六、std::move()
std::move()
能把左值強制轉換為右值。
我們把語句 Integer b(temp);
改為 Integer b(std::move(temp));
後,執行結果如下。
Call Integer(int value)
Call Integer(Integer&& source)
Call ~Integer()
100
-----------------
Call Integer(int value)
Call Integer(Integer&& source)
10000
-----------------
Call ~Integer()
Call ~Integer()
Call ~Integer()
從“10000”的上一行可以看出,std::move()
確實把左值 temp
轉換為右值。