[讀書筆記]Universal References in C++11 part 4: Reference Collapsing, Perfect Forwarding and Universal Reference

 

Universal,這個字在其實是一種抽象的表達。任何抽象的表達,在下面大多會有一套自動化的機制,來將本該由使用者操作的複雜概念隱藏起來。Universal References可以說是一種抽象的介面,它提供了一個統一的窗口給lvalue reference以及rvalue reference。但是從上面幾篇文章我們不是才知道,將這二個reference的概念區分出來就是為了要減少暫存物件記憶體操作的成本嗎?現在說要把它們又合在一起不是本末倒置了嗎?──當然不會,這裡所說提供的統一窗口,是為了給在設計功能時,還不知道傳入物件型態的,需要在編譯期推導出來的情況下使用。

 

想想看,在什麼情況下會有這種情形?其實這種實作在標準函式中到處都是。應該呼之欲出了吧?答案是template。

 

template又稱為編譯期靜態多型,跟使用繼承方式的執型期動態多型的方式不同,它會在我們按下編譯的時候才將需要的程式碼產生出來。雖然介面上看起來只有一個,但實際上它在底下可是產生了lvalue reference跟rvalue reference二個版本。這種性質可以說是一種拯救;它讓我們程序猿們省下了不小維護程式碼的心力,因為,這樣我們就不需要對每個member variable製作二個不同的版本的setter。例如,若沒使用universal reference的程式碼會是下面的情況:

 

class Widget {
public:
    Widget(){};

 
 
    void setName(const std::string& newName) 
    {
        m_name = newName;
    } // const lvalue
    void setName(std::string&& newName)
    {
        m_name = std::move(newName);
    } // rvalue
 
    void setResource(std::shared_ptr<Resource>&  resourcePtr){
        m_resourcePtr = resourcePtr;
    }
    void setResource(std::shared_ptr<Resource>&&  resourcePtr){
        m_resourcePtr = std::move(resourcePtr);
    }
 
private:
    std::string m_name;
    std::shared_ptr<Resource> m_resourcePtr;
};


int main(){

    {
        Widget widget;

        widget.setName("rvalue reference");
        std::string name = "lvalue reference";
        widget.setName(name);


        widget.setResource(std::make_shared<Resource>());
        auto resourcePtr = std::make_shared<Resource>();
        widget.setResource(resourcePtr);
    }


    return 0;
}

 

用了之後馬上減少2個方法的實作:

class URWidget{
public:
    URWidget(){};


    template<typename T>
    void setResource(T&& newResoure) // universal reference
    {
        //name = std::move(newName);  //bad
        m_resourcePtr = std::forward<T>(newResoure);
    }


    template<typename T>
    void setName(T&& newName) // universal reference
    {
        m_name = std::forward<T>(newName);
    }

private:
    std::string m_name;
    std::shared_ptr<Resource> m_resourcePtr;

};


int main(){


    {

        URWidget widget;

        widget.setName("rvalue reference");
        std::string name = "lvalue reference";
        widget.setName(name);


        widget.setResource(std::make_shared<Resource>());
        auto resourcePtr = std::make_shared<Resource>();
        widget.setResource(resourcePtr);
    
    }

    return 0;
}

 

這麼好的性質,難道不用嗎?由於Universal Reference傳入的參數可以是任何版本的reference,所以要特別注意到的是我們必須包上std::forward這一層:若傳入的是lvalue,則呼叫lvalue reference版本;若傳入的是rvalue,則呼叫rvalue reference版本。有人可能會問,為什麼我在上面的code中把std::move註解掉了,並打上一個bad。這是因為,若傳入的是rvalue則一切運行如常,但若傳入的是lvalue,則可能會造成使用的時候再次呼叫該物件而造成程式崩潰。例如,當我們在呼叫完widget.setResource(resourcePtr)之後,再次去呼叫resourcePtr的情況。若是這裡是真的想要把resourcePtr轉移給widget的話,則必須要再包上一層std::move表明:widget.setResource(std::move(resourcePtr))。

 

另外必須要注意的是,若用下面這種方式寫的話,會在傳入lvalue的時候發生編譯錯誤:

template<typename T>
class TWidget{
public:
    TWidget(){};


    void setResource(T&& newResoure)
    {
        //name = std::move(newName);  //bad
        m_resourcePtr = std::forward<T>(newResoure);
    }


    void setName(T&& newName)    {
        m_name = std::forward<T>(newName);
    }

private:
    std::string m_name;
    std::shared_ptr<Resource> m_resourcePtr;

};


int main(){
    {
        TWidget<std::shared_ptr<Resource>> widget;
        widget.setResource(std::make_shared<Resource>());
        auto resourcePtr = std::make_shared<Resource>();
        widget.setResource(resourcePtr);//compile error, rvalue reference version can't bind lvalue 
    }
    return 0;
}

這是因為setResource的版本在widget建出來的時候就已經產生完畢,在這裡的例子由於只有產生rvalue的版本,因此在呼叫lvalue的時候就會編譯錯誤。讓我們再多做一些實驗看看會怎麼樣:

 

int main(){
    {
        TWidget<const std::shared_ptr<Resource>&> widget;
        widget.setResource(std::make_shared<Resource>());//will call lvalue reference version
        auto resourcePtr = std::make_shared<Resource>();
        widget.setResource(resourcePtr); //will call lvalue reference version
    }

    {
         TWidget<std::shared_ptr<Resource>&&> widget;
         widget.setResource(std::make_shared<Resource>());
         auto resourcePtr = std::make_shared<Resource>();
        // widget.setResource(resourcePtr); //compile error, rvalue reference version can't bind lvalue
    }

    return 0;
}

結果很奇妙,為什麼TWidget<std::shared_ptr<Resource>&> widget; 跟TWidget<std::shared_ptr<Resource>&&> widget;能夠編譯,而且產生的setResource的版本還不一樣。在試著將template的參數帶入TWidget之後,我們會發現setResource變成很奇怪的形態:

 

void setResource(std::shared_ptr<Resource>& && newResoure)

void setResource(std::shared_ptr<Resource>&& && newResoure)

 

這東西有辦法編譯?相信大多數人看到的第一個感想會是這樣,但神奇的是編譯器不但能編譯而且還正常運行著其產生的版本。當然,若是你試著用不是template的方式去將function寫出來,編譯器馬上會叫錯誤給你看。其實這是為了解決template可以帶入任何參數的問題,而由編譯器所導入的一個機制:Reference Collapsing。它的規則如下:

  • A& & becomes A&
  • A& && becomes A&
  • A&& & becomes A&
  • A&& && becomes A&&

 

所以在這裡的例子,我們的void setResource(std::shared_ptr<Resource>& && newResoure) 變成了void setResource(std::shared_ptr<Resource>& newResoure) ;setResource(std::shared_ptr<Resource>&& && newResoure) 則變成了setResource(std::shared_ptr<Resource>&& newResoure) 。看吧,一切又變成了如往常的美好。也就是因為這個機制,才能讓Universal Reference不受template帶入參數的影響,完成Perfect Forwarding:

 

//perfect forwarding
{
    URWidget widget;
    widget.setResource<std::shared_ptr<Resource> >(std::make_shared<Resource>());//call rvalue version

    auto resourcePtr = std::make_shared<Resource>();
    widget.setResource<std::shared_ptr<Resource>& >(resourcePtr); //call lvalue version
    widget.setResource<std::shared_ptr<Resource>&& >(std::move(resourcePtr));//call rvalue version

}

 

 

最後,在明白了move語句的強大之處後,或許有人會想要這樣應用:

static URWidget makeWidget() // Moving version of makeWidget
{
    URWidget w;
    //do somthing
    return std::move(w); // move w into return value
} // (don't do this!)

 

嗯,直接把local object轉移給回傳的暫存物件,挺好的不是嗎?看起來是很理想,但由於編譯器有Return Value OptimizationNRVO的機制,因此若回傳值被包上任何function的話,這個機制就不會被啟動。雖然move還蠻快的,但速度上還是比不上RVO,因此不要在RVO機制啟動的情況下包上一層std::move,因為速度不會變快。當然,還是會有需要的情況,但這通常是因為RVO啟動的條件沒達成。

 

 

最後,本系列的程式碼,可在github上下載:網址在此

2 Responses

  1. Anonymous

    setName使用
    void setName(const std::string& newName)
    void setName(std::string&& newName)
    會比你使用
    template
    void setName(T&& newName)
    還要好

    如果使用template版本,表示的是”任意型態”皆可以用來setName

    • dorgon

      其實這就是設計上的選擇

      有時候過度設計並不是好事,
      因為很有可能會造成語意上理解的困難

      很多時候是看專案實際上的需求跟maintain的人來決定實作的方式。

      老實說我並不會想要在跟人合作的專案中導入這裡提到的概念…
      因為大部份的人都對C++11這些特性還不熟悉。

      這邊的程式是展示概念的用途居多: )

Leave a Reply

Your email address will not be published. Required fields are marked *