Value Category(值類別)- 2 / 2

為了方便閱讀,分成兩個部分。可以往上翻閱來知道為什麼要這樣演進。
第一部分:Back in C & Temporary Materialization Conversion
第二部分:Modern C++ Value Category

上一部份停在 Temporary Materialization Conversion (TMC)。首先會從 reference binding 講起,再進入 Modern C++ 與例子。

Pass-by-reference

在 C++ 想要傳值進去函數裡面。可以有三種方式:

  • pass-by-value:會複製一份參數在函數的 scope 當中,out-of-scope 後 pop stack
  • pass-by-pointer:傳入參數值的記憶體位置
  • pass-by-reference:基本上與 pass by pointer 無異,可以想像是每次在使用參數值時它會自動幫你 dereference 一次。
void f ( int& x ); 
void f ( Obj& x ); 
(1) using my_type = int; 
(2) using my_type = Obj; 
my_type var; f(var); 

這樣設計提供一個對程式設計更友善的介面,讓傳入 immediate 與 class type 兩者的方式沒有差異。Reference 增加了函數介面的可讀性,讓 overloaded operators 可以直接抓取物件,就像是內建運算子一樣。而 C++ 也期待你把 class-type 設計的像是 built-in type 一樣。

舉例來說,在 C 中以下片段可以被編譯並執行。

enum Month {
  Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec, month_end
};
typedef enum Month Month;
for (Month m = Jan; m <= Dec; ++m);

但是 C++ 並不允許 built-in ++ on enumeration type。所以我們需要自己寫一個。但是如果沒有 reference 這個語意的話,你會發現 for-loop 寫不出來,因為 ++ 這個運算子不能有 pointer 作為參數。

這裡我們先繼續留在 C++03,TMC 還沒有出現之前。我們無法對 rvalue 做 reference binding。

對一個 non-modifiable lvalue。它們無法出現在等號的左側。在一個函數中做 reference to constant 來確保「在函數中不會對參數值做任何變動」,其實這件事的「外顯行為」其實與 pass-by-value 沒有任何差別,它們都不會對傳入的值進行更動。

Rvalue reference

而如果需要對一個物件 bind reference,那麼就需要他的記憶體位置。回顧上一篇,我們提到 TMC 將 rvalue 轉換為暫時有效的物件。原本

在 C++03 時,”A reference yields an lvalue”這句話是成立的。因為對 reference 我們會有賦值的操作。而對 rvalue 沒有辦法。Reference 也無法被 bind 到 rvalue 上。

但是回顧上一篇,我們提到 TMC 將 rvalue 轉換為暫時有效的物件,現在 rvalue 可以作為物件了,從 C++11 開始有了 TMC,也允許對 rvalue 做 reference binding。

有了 TMC 創造的暫時物件,C++11 允許將物件以 const reference 傳入函數就是因為這樣 “pass-by-reference” 的「外顯行為」與 pass-by-value 沒有差別,而且這樣還省去了 pass-by-value 中複製帶來的額外花費。

要使用 rvalue (trigger TMC)時,需要使用 && 。這時 rvalue 會轉為 xvalue。編譯器就可以藉此知道物件之後會被銷毀。

Take rvalue as xvalue(以 std::string 為例)

std::string 中我們要做到 s1 = s2 + s3 時,結果該如何被覆寫到 s1

我們要把結果放在 s2 還是 s3 之中?有了 TMC, s2 + s3 這個 rvalue 作為物件,透過 && 轉為 xvalue 後,可以使用 move assignment 把值搬移到 s1,既安全又節省空間的。

class string {
public: 
  string operator+(string&, string&); // simplified '+' operator
public:
  string(string const&);                 // copy constructor
  string& operator=(string const &);     // copy assignment
  string(string&&);                      // move constructor
  string& operator=(string &&) noexcept; // move assignment
};

Take lvalue as xvalue(以 std::swap 為例)

這是原本的 swap。語意上來說,a 的值在被送去 temp 之後無效,但隨之從 b 又搬移過來值,b 再從 temp 承接內容。

template<typename T>
void swap(T& a, T& b){
  T temp(a);
  a = b;
  b = temp;
}

編譯器無法知道「值即將無效(expire)」這件事,這時就可以用 std::move 來將它標示為 xvalue,讓編譯器知道這個值是可以被搬移的。

std::move 標頭 (simplified)

template<typename T>
constexpr T&& std::move(T&& a); // this is a simplified version

有了 std::move 之後,C++11 的 std::swap 就會長得像下面這樣。透過 std::move 讓編譯器知道可以把 lvalue 拿來變成 xvalue,並且對 lvalue 參數實際資料做搬移,增加效率。

template<typename T> void swap(T& t1, T& t2) {
  T temp(std::move(t1));
  t1 = std::move(t2);
  t2 = std::move(temp);
}

小結

當撰寫程式時,雖然現在知道有 TMC 這一回事,但是在使用 rvalue 時要假設它們不佔有記憶體空間,工程師需要記住這個假設。其實這個道理就像編譯器對 Llvalue 做的事一樣。編譯器可能會對物件的週期做優化讓他們的記憶體空間被重複,但是在撰寫程式時,使用者使用 lvalue 的假設是這些是佔有記憶體空間的物件。

如果你覺得有收穫,可以用 30 NTD 來支持我繼續創作更多內容。因為做自己喜歡的事而得到報酬,是再好不過的事了。(街口支付)

Author: eopXD

Hi 我是 eop ,希望人生過得有趣有挑戰XD

One thought on “Value Category(值類別)- 2 / 2”

Leave a Reply