2014年2月18日

[C++] rvalue reference & implementation class

之前在這篇文章有簡單的聊過 rvalue reference 這東西,當初標準會定義這個簡單來說就是為了效能,特別是 C++ 常常會暗地裡幫你做了一大堆你根本沒想到的事情,因此善用 rvalue reference 可以促使 compiler 提升程式的整體效能 -- 特別是避免無意義的複製這方面。

這篇主要是談 rvalue reference + implementation class 的結合可以產生什麼樣的效果,在 C++,由於 header file 必須定義好 class 的 declaration -- 包含使用者不需要知道的 private members,因此創出了 implementation class 這東西負責撰寫並處理原始 class 要做的事情,而要公開的另一個 class 就只有一個 private member 是 implementation class 產生的 object,而 public member 就是 implementation class 的 public interface。這個小技巧原本就如同前面所說,只是個用來 client 不會看到不用知道的東西,除此之外確實是沒什麼效果,但是 rvalue reference 的出現讓這個效果可以有提升效能的作用。


關於 implementation class 還是先來舉個例吧! 假設原本想要寫出一個這樣的 class

class A
{
private:
    int x;
    void reset();

public:
    void setX(const int new_x);
    int getX() const;
};

利用 implementation class 的話可以改寫成這樣

class AImpl
{
private:
    int x;
    void reset();

public:
    void setX(const int new_x);
    int getX() const;
};

class A
{
private:
    AImpl* aimpl;

public:
    A();
    A(const A& rhs);
    ~A();
    A& operator= (const A& rhs);
    inline void setX(const int new_x) const { aimpl->setX(new_x); }
    inline int getX() const { return aimpl->getX(); }
};

如此一來就可以把 AImpl 藏在別的地方,client 就只看得到 A 裡的東西囉! 這邊我把 ctor、copy ctor 還有 dtor 的實作部分拿掉不寫只是因為我懶 + 那邊確實沒什麼好說的XD assignment operator 這邊比較有趣一點,所以我拿出來另外寫:

A& A::operator= (const A& rhs)
{
    A temp = new A(rhs);
    swap(aimpl, temp.aimpl);
    return *this;
}

這邊 tricky 的地方在這裡:原本 assignment operator 要避免 self assignment 並且寫出類似這樣的東西:

if(this == &rhs) return *this;

這種寫法可以省掉上面那樣的檢查條件,並且還可以滿足 strong exception safty condition -- commit or rollback,不過我這邊把 exception 的部分拿掉了,請自行補上XD。關於這部分之後再找時間補充,C++ 的 exception 有滿多東西可以講的,特別是在 class 架構設計上以及 exception safty 的議題討論上,因為 exception 的影響甚巨,如果沒有在設計架構的開始就納入考量,事後也難以,甚至是無法補上的。

至此為止是 implementation class 的一般形式,rvalue reference 的影響主要在兩個:copy ctor 以及 assignment operator,考慮以下情況 (假設 A 有 overload operator+):

A w, x, y, z;
1. w = x + y + z;
2. A a = x + y + z;

注意,第二種情形最後的 assignment 是呼叫 copy ctor 而不是 assignment operator。
首先我們知道這種加法運算會產生許多的暫時物件,所以上一篇文章有提到說可以利用 rvalue reference 來減少複製的次數。這邊有趣的點在最後一個 assignment:如果今天 assignment operator 是像上面那樣定義,當然這個 assignment 也還會有一次複製的行為,但其實仔細想想就會發現,那個複製根本沒有必要! 原因如下:
1. A 裡面的實作改用 implementation class,所以只有一個指標 aimpl
2. 暫存物件一定會被摧毀 (給個名稱比較方便,就取成 tmp 吧!),但是如果有辦法把 w 跟 tmp 的 aimpl 給交換,而且 w 的 aimpl 也本來就要被銷毀,如此一來就可以避免這次這次的複製,從而省下不少時間!

rvalue reference 讓上面所說的想法得以實現,只需準備以下兩個 function

A& A::operator= (A&& rhs)
{
    swap(aimpl, rhs.aimpl);
    return *this;
}

A(A&& rhs) : aimpl(NULL)
{
    swap(aimpl, rhs.aimpl);
}

如此一來,當 compiler 發現可以調用上面那兩個版本的 function 時 (也就是有暫存物件產生時),就會直接將他們內部的指標作交換,從而避免多餘的複製行為。如果對 C++ 的敏感度夠高的話,會發現 rvalue reference 挺好用的,可以在一些小細節降低負擔,從而提高整體程式的效能。

沒有留言:

張貼留言