C++11的版本在類型系統上下了很大的功夫,添加了諸如auto,decltype,move等新的關鍵詞來簡化代碼的編寫與降低閱讀代碼的難度。為了更好的理解這些新的語義,筆者確定通過幾篇文章來簡單窺探一下C++類型系統的冰山一角,如果加深了對C++類型系統的理解,想必大家也能夠更好的應用由C++11帶給我們的新"利器"。
1.左值與右值
左值(lvalue)和右值(rvalue)是C++類型系統之中的基礎概念,我們不需要了解這些基礎概念,同樣也能寫出代碼。但是如果沒有弄清左右值的概念,對於許多C++高級特性的探索會一葉障目,所以筆者嘗試總結一下自己對於左值與右值的理解。
在C++11之前的版本,基本沿用了C語言之中對於左值與右值的定義,說起來也很簡單:“在C++之中的變量只有左值與右值兩種:其中凡是可以取地址的變量就是左值,而沒有名字的臨時變量,字面量就是右值”。 正是因為這兩種變量分別位於=的左右兩側,所以被命名為左值與右值。下面,舉個栗子:
int x;
int y;
x = 1;
y = 2;
x = y;
y = x;
// 以下代碼有誤
3 = x;
x + y = 4;
通過上述的代碼我們可以快速的理解,顯然x,y作為變量可以存在=的左側,而稱之為左值,而3,x + y作為字面量或中間結果,沒有辦法取得地址,則稱之為右值。 這里筆者也給一個簡單判定的左右值的方式:
判斷能否取值的地址,能取地址的就是左值。
2.將亡值
其實上一節對於左值右值的描述,在我們編寫絕大多數代碼的場景下並沒有什么影響。而在C++11擴展了右值的的概念,將右值分為了純右值(pure rvalue)與將亡值(eXpiring Value)。純右值的概念等同於我們之前所理解的右值,指的是臨時變量或字面量值;而將亡值是C++11新引入的概念,它依托於右值。
在C++之中,使用左值去初始化對象或為對象賦值時,會調用拷貝構造函數或賦值構造函數。而使用一個右值來初始化或賦值時,會調用移動構造函數或移動賦值運算符來移動資源,從而避免拷貝,提高效率。 而將亡值可以理解為通過移動構造其他變量內存空間的方式獲取到的值。在確保其他變量不再被使用、或即將被銷毀時,來延長變量值的生命期。而實際上該右值會馬上被銷毀,所以稱之為:將亡值。
上述概念闡述的略微抽象,我們來看下面這段代碼:
這是我們簡單定義的Time類,在類中我們定義了拷貝構造函數和移動構造函數:
class Time {
public:
int* hour;
int* minute;
int* second;
Time(int h, int m, int s) {
hour = new int(h);
minute = new int(m);
second = new int(s);
}
Time(const Time& t) {
cout << "copy" << endl;
hour = new int(*t.hour);
minute = new int(*t.minute);
second = new int(*t.second);
}
Time(Time&& t) noexcept:hour(t.hour),minute(t.minute),second(t.second) {
t.hour = nullptr;
t.minute = nullptr;
t.second = nullptr;
cout << "move" << endl;
}
~Time() {
cout << "call ~Time()" << endl;
delete hour;
delete minute;
delete second;
}
};
接下來我們執行下面的代碼:
int main()
{
Time test(10,25,12);
Time test2(test);
return 0;
}
執行結果:
copy
call ~Time()
call ~Time()
由上述代碼我們看到test2對象調用了拷貝構造函數來構造了新的對象,這個過程顯然是更占用內存的。而接下來,我們嘗試利用move函數將test強行轉化為將亡值,來避免內存重新分配的過程。但是之后我們也無法再訪問test對象的內容了,因為都在移動構造函數之中置為了空指針。
int main()
{
Time test(10,25,12);
Time test2(move(test));
return 0;
}
執行結果:
move
call ~Time()
call ~Time()
通過這樣的方式來減少不必要的內存操作。但是之后我們也無法再訪問test對象的內容了,因為都在移動構造函數之中置為了空指針。將亡值通過移動構造函數”借屍還魂“,通過test2變量延續了自己的生命周期。
3.左值的一些"坑"
雖然筆者給出了左右值分辨的一些基本標准,但是還是有下面一些很讓人迷惑的場景:
- 條件表達式返回左值
true ? i : i;
- ++
i++ // 左值
++i // 右值
- []數組取值返回左值
i[10]
- 指針取值操作符返回左值
*i
- 字符串字面量返回左值
“hello world”
這是一些表示左值的特殊情況,這里筆者也不展開一一贅述了,希望大家可以簡單的進行記憶。當然,筆者從來不去記一些太瑣碎的問題,因為太他喵難記了,所以在C++11之中,可以標准庫中添加的模板類is_lvalue_reference來判斷表達式是否為左值,is_rvalue_reference來判斷是否為右值。
cout << is_lvalue_reference<decltype(i[10])>::value << endl;
cout << is_rvalue_reference<decltype(i[10])>::value << endl;
返回1則為真,0為假。
4.小結
這只是我們對C++類型系統的第一篇探討,后續筆者還會繼續深入的探討有關C++11新特性之中與類型系統相關的內容,歡迎大家多多討論,指教。