TAG

首都機能移轉 (2) 歌詞 (2) 靠北文 (40) 戲言 (30) 糟糕 (7) ACG (23) Assembly (2) Boost (2) C (31) C++ (69) CMake (4) CSIE (67) Debian (34) Design_Pattern (2) Django (1) Eclipse (1) en_US (13) FFmpeg (3) FoolproofProject (26) FreeBSD (2) Git (4) GNU_Linux (65) IDE (5) Java (11) JavaScript (19) KDE (15) Khopper (16) KomiX (3) Kubuntu (18) Life (1) Lighttpd (2) Mac_OS_X (2) Opera (1) PHP (2) PicKing (2) Programing (21) Prolog (1) Python (7) QSnapshot (2) Qt (30) Qt_Jambi (1) Regular_Expression (1) Shell_Script (7) Talk (98) VirtualBox (7) Visual_Studio (13) Windows (18) zh_TW (36)

2012年4月18日 星期三

About the rvalue reference

之前拜讀過 yoco315 所翻譯的 Rvalue References 文章, 把原本相當複雜的概念寫得非常淺顯易懂, 讓我當時忍不住手癢實驗了一下.
由於這是一個全新的特性, 當時還沒有很完整的 coding standard 可參考, 因此越寫疑惑就越深, 最後就先把注意力轉移到其他特性, 如 lambda expression.
今天的 OSDC.tw 很幸運地遇到 fr3@K 給了一場 C++11 的 talk, 讓我又再次燃起熱血, 便藉著指導新生的機會也來順便練習 class design.
首先要說的是, 使用 pimpl idiom 的 classes 較容易從 rvalue reference 和 swap 受益. 原因無它, handle classes 的成員變數只有一個指標, 無論是 move 或是 swap 都只要改變指標的值即可, 無須額外的 allocation 即可達成目的. 另外一個巨大的好處是可以讓實作與介面真正的分離, 這在一個大型的 C++ project 可以省下非常多的編譯時間. 未使用 pimpl idiom 的 classes 通常會比較麻煩一點, 因為很可能不是所有的 member variable 都被實作為 movable.
那麼要如何實作一個 movable 的 class? 最少必須要實作 move constructor 以及 overload move assignment operator. e.g.:
class BigInteger {
public:
    BigInteger( BigInteger && that );
    BigInteger & operator =( BigInteger && that );
private:
    class Private;
    std::unique_ptr< Private > p_;
};
Move constructor 的實作很簡單, 把別人的 member variable 全搶過來就對了:
BigInteger::BigInteger( BigInteger && that ): p_( move( that.p_ ) ) {
}
Move assignment operator 的邏輯也一樣, 反正 rvalue 之後就會蒸發, 你可以隨意惡搞它:
BigInteger & BigInteger::operator =( BigInteger && that ) {
    this->p_.swap( that.p_ );
    return *this;
}
注意這和一般版本的 assignment operator 不同, 你不需要也不能做證同測試, 因為每個 rvalue 都是不一樣的, 而且 rvalue 也無法取址.
好的, 這就是所有我們所需要的; 但是不是所有的函式都應該加上 rvalue reference 版本的 overloading 呢? 基本上, 我摸索出來的大原則是, 只有當你需要偷別人的 resource 來建立一個新複本時才需要 rvalue reference. 舉例來說, 這樣子的 operator 就沒必要使用 rvalue reference:
bool operator ==( const BigInteger & a, const BigInteger & b );
因為這個動作是 read-only, 並且沒有必要複製物件. 但當你需要回傳一個新的物件時, 你可以慎重考慮增加 rvalue 支援. 為什麼說要慎重考慮呢? 以 binary operators 為例:
BigInteger operator +( const BigInteger & a, const BigInteger & b );
BigInteger operator +( const BigInteger & a, BigInteger && b );
BigInteger operator +( BigInteger && a, const BigInteger & b );
BigInteger operator +( BigInteger && a, BigInteger && b );
你必須要為四種不同的組合個別提供 overloading version. 第四個版本其實沒多大意義, 因為就算兩個 operand 都是 rvalue, 最後還是只能 move 其中一個 operand. 但沒有這個版本, 在兩端都是 rvalue 的使用條件下會出現岐義, 因為 compiler 不確定要把哪一端 bind 成 lvalue.
另外, 和 RVO (Return Value Optimization) 的 concept 不同, RVO 是 compiler 的一個最佳化技巧, 它是直接消除 copy constructor 的呼叫動作, 但 move semantic 的宗旨是廉價的 copy, 它還是需要呼叫 constructor, 只是這個 constructor 的成本很低. 當你需要 move semantic 時, 請記得使用 std::move, 否則 compiler 會視狀況呼叫 copy constructor:
T T::function() {
    // return T(); <- may calls T( const T & )
    return std::move( T() ); // calls T( T && )
}
缺憾是目前 N2439 還沒有 compiler 實作出來, 不然就可以 move *this:
class BigInteger {
public:
    BigInteger operator -() const &;
    BigInteger operator -() const &&;
};
第二個版本可以在自己就是 rvalue 的情況下直接把自己 move 出去, 其他的情況就乖乖得複製一份複本.

5 則留言:

  1. BigInteger::BigInteger( BigInteger && that ): p_(move(that.p_) ) {
    }

    回覆刪除
  2. 噢對, 這樣也可以!

    我看到 MSDN 上的另一個寫法是

    *this = std::move( that );

    好像也很省事, 只是 member 就不能在 initialize list 做 move 了

    回覆刪除
  3. *this = move(that); // ???

    Bad smell.

    回覆刪除
  4. http://msdn.microsoft.com/en-us/library/dd293665.aspx

    In the page end.

    回覆刪除
  5. Bad smell confirmed.

    其實好的作法是反過來的. 用 move ctor 去實現 move assignment op.

    另外, 在 ctor 裏面用 member initialization list 也是大家都該知道的 best practice.

    回覆刪除