1. 程式人生 > 實用技巧 >10C++11通用為本,專用為末

10C++11通用為本,專用為末

1. 繼承建構函式
struct A
{
    A(int i){}
    A(double d, int i){}
    A(float f, int i, const char* c){}
};

struct B : public A
{
	using A::A;   //繼承建構函式
	virtual void extraInterface(){}
}

​ 這裡我們通過 using A::A 的宣告,把基類中的建構函式悉數整合到派生類 B 中。這樣就不需要要再為派生類定義多個構造函數了。

2. 委派建構函式(主要用於多個過載建構函式中)
class Info
{
public:
    Info(){initRest()};
    Info(int i):type(i) {initRest();}
    Info()(char e):name(e) {initRest();}
    
private:
    void initRest(){}
    int type {1};
    char name {'a'};
}

​ 上例中,每個建構函式都需要呼叫 initRest 函式進行初始化。而現實程式設計中,建構函式中的程式碼還會更長,比如可能還需要一些基類的建構函式等。那能不能在一些建構函式中連 initRest 都不用呼叫呢?

​ 在 c++11 中,我們可以使用委派建構函式來達到預期的效果。

class Info
{
public:
    Info(){initRest()};
    Info(int i):Info() { type = i;}
    Info()(char e):Info() {name = e;};
    
 private:
    void initRest(){}
    int type {1};
    char name {'a'};
}

​ 上例中,我們在 Info(int) 和 Info(char) 的初始化列表的位置,呼叫了“基準版本”的建構函式 Info()。這裡我們為了區分被呼叫者和呼叫者,稱在初始化列表中呼叫“基準版本”的建構函式為 委派建構函式(delegating construnctor),而被呼叫的“基準版本”則為目標建構函式(target constructor)。

​ 在 C++11 中,所謂委派構造,就是指委派函式將構造的任務委派給了目標建構函式來完成這樣一種類構造的方式。

​ 注意:在 C++ 中,建構函式不能同時 “委派”和使用初始化列表。

3. 移動語義

​ 拷貝建構函式中未指標成員分配新的記憶體再進行內容拷貝的做法在 c++ 程式設計中幾乎被視為不可違背的。不過有些時候,我們確實不需要這樣的拷貝構造語義,這時候就可以使用 C++ 提供的移動語義。

#include <iostream>
using namespace std;

class HasPtrMem
{
 public:
 	HasPtrMem() : d(new int (3)) 
    {
        cout << "Construct:" << ++ n_cstr << endl;
    }
    HasPtrMem(const HasPtrMem& h): d(new int(*h.d))
    {
        cout << "Copy construct:" << ++n_cptr <<endl;
    }
    //移動建構函式   
    HasPtrMem(HasPtrMem && h): d(h.d)
    {
        h.d = nullptr;			//將臨時值的指標成員置空
         cout << "Move construct:" << ++n_mvtr <<endl;
    }
    ~HasPtrMem()
    {
        delete d;
        cout << "Destruct:" << ++n_dstr <<endl;
    }
    
    int * d;
    static int n_cstr;
    static int n_dstr;
    static int n_cptr;
    static int n_mvtr;
    
};
int HasPtrMem::n_cstr = 0;
int HasPtrMem::n_dstr = 0;
int HasPtrMem::n_cptr = 0;
int HasPtrMem::n_mvtr = 0;

HasPtrMem getTemp()
{
    HasPtrMem h;
    cout << "Resource from " << __func__ << ": " << hex << h.d << endl;
    return h;
}

int main()
{
    HasPtrMem a = getTemp();
    cout << "Resource from " << __func__ << ": " << hex << a.d << endl;
}

​ 上例中,HasPtrMem( HasPtrMem &&) 就是所謂的移動建構函式。與拷貝建構函式不同的是,移動建構函式接受一個所謂的 “右值引用” 的引數。可以看到,移動建構函式使用了引數 h 的成員 d 初始化了本物件的成員 d (而不是像拷貝建構函式一樣需要分配記憶體,然後將內容依次拷貝到新分配的記憶體中),而 h 的成員 d 隨後被置為指標空值 nullptr。這就完成了移動構造的全過程。

​ 這裡所謂的 “偷” 堆記憶體,就是指將本物件 d 指向 h.d 所指的記憶體這一條語句,相應地,我們還將 h 的成員 d 置為指標空值。這其實也是我們 “偷” 記憶體時必須做的。這是因為在移動構造完成之後,臨時物件會立即被析構。如果不改變 h.d (臨時物件的指標成員)的話,則臨時物件會析構掉本是我們 “偷” 來的堆記憶體。這樣一來,本物件中的 d 指標也就成了一個懸掛指標,如果我們對指標進行解引用,就會發生嚴重的執行時錯誤。

​ 那麼還有一個最為關鍵的問題沒有解決,那就是移動建構函式何時會被觸發。之前我們只是提到了臨時物件,一旦我們用到的是個臨時變數,那麼移動建構函式就可以得到執行。

​ 那麼,在 C++ 中如何判斷產生了臨時物件?如何將其用於移動建構函式?是否只有臨時變數可以用於移動構造?

4. 左值、右值與右值引用
4.1 左值與右值

​ 左值、右值的最為典型的判斷方法就是,在賦值表示式中,出現在等號左邊的就是 “左值”,而在等號右邊的,則稱為 “右值”。

​ 另一個被廣泛認同的說法,那就是可以取地址的、有名字的就是左值,反之,不能取地址的,沒有名字的就是右值。

​ 在 c++11 中,右值是由兩個概念構成的,一個是將亡值(xvalue),另一個則是純右值(prvalue)。

​ 其中純右值就是 c++98 標準中右值的概念。比如非引用返回的函式返回的臨時變數值就是一個純右值。一些運算表示式,比如 1+3 產生的臨時變數值,也是純右值。而不跟物件關聯的字面量值,比如 2、'c'、true 也是純右值。此外,型別轉換函式的返回值、lambda 表示式等,也都是右值。

​ 而將亡值則是 c++11 新增的跟右值引用相關的表示式,這種表示式通常是將要被移動的物件(移為他用),比如返回右值引用 T&& 的函式返回值、std::move 的返回值,或者轉換為 T&& 的型別轉換函式的返回值。

​ 而剩餘的,可以標識函式、物件的值都屬於左值。 在 c++ 程式中,所有的值必屬於左值、將亡值、純右值三者之一。

4.2 右值引用

​ 在 c++11 中,右值引用就是對一個右值進行引用的型別。事實上,由於右值通常不具有名字,我們也只能通過引用的方式找到它的存在。通常情況下,我們只能是從右值表示式獲得其引用。比如:

T && a = returnRvalue();
//這個表示式中,假設 returnRvalue 返回一個右值,我們就宣告可一個名為 a 的右值引用,其值等於 returnRvalue 函式返回的臨時變數的值。

​ 為了區別與 c++98 中的引用型別,我們稱 c++98 中的引用為 “左值引用”。右值引用和左值引用都是屬於引用型別。無論是宣告一個左值引用還是右值引用,都必須立即進行初始化。而其原因可以理解為是引用型別本身自己並不擁有所繫結物件的記憶體,只是改物件的一個別名。最值引用是具名變數值的別名,而右值引用則是不具名(匿名)變數的別名。

​ 在上面的例子中,returnRvalue(); 函式返回的右值在表示式語句結束後,其生命也就終結了,而通過右值又 “重獲新生”,其生命期將與右值引用型別比變數 a 的生命期一樣。只要 a 還 “活著”, 該右值臨時量將會一直 “存活”下去。

T b = returnRvalue();

​ 相比以上語句的宣告方式,我們剛才的右值引用變數宣告,就會少一次物件的析構及一次物件的構造。因為 a 是右值引用,直接綁定了 ReturnRvalue() 返回的臨時量。而 b 只是由臨時值構造而成的,臨時值在表示式結束後會析構,因此就會多一次析構和構造的開銷。

4.3 常量左值引用

​ 相對的,在 c++98 標準中就已經出現的左值引用是否可以繫結到右值(由右值進行初始化)呢?例如:

T & e = returnRvalue();			//編譯出錯
const T & f = returnRvalue();	//通過編譯
//出現這樣的狀況的原因是,常量左值引用就是個“萬能”的引用型別。它可以接受非常量左值、常量左值、右值對其進行初始化
//而且在使用右值對其進行初始化的時候,常量左值引用還可以像右值引用一樣將右值的生命期延長。
//不過相比於右值引用所引用的右值,常量左值所引用的右值在它的“餘生”中只能是隻讀的。
//相對地,非常量左值只能接受非常量左值對其進行初始化。

​ 在 c++11 之前,左值、右值對程式設計師來說,一致呈透明狀態。不知道什麼是左值、右值,並不影響寫出正確的 c++ 程式碼。引用的是左值和右值通常也並不重要。

​ 為了語義的完整, c++11 中還存在著常量右值引用,比如我們通過以下程式碼宣告一個常量右值引用、

const T & crvalueref = returnRvalue();
//但是,一來右值引用主要就是為了移動語義,而移動語義需要右值是可以被修改的,那麼常量右值引用在移動語義中就沒有用武之處了。
//而來如果要引用右值且讓常量右值引用不可以更改,常量左值引用往往就足夠了。因此,目前我們還沒有看到常量右值引用有何用處。
4.4 小結

​ 有時候,我們可能不知道一個型別是否是引用型別,以及是左值引用還是右值引用(這在模板中比較常見),標準庫在 <type_traits> 標頭檔案中提供了 3 個模板類: is_rvalue_reference、is_lvalue_reference、is_reference,可供我們進行判斷。

cout << is_rvalue_reference<string &&>::value;

​ 我們通過模板類的成員 value 就可以打印出 string&& 是否是一個右值引用了。配合第四章中的型別推導操作符 decltype, 我們甚至可以對變數的型別進行判斷。