C++: 左值引用(&), 右值引用(&&),萬能引用(template &&)詳解 與 完美轉發(forward) 實現剖析


2.正文

2.1 左值引用(&)與右值引用(&&)

在c++11中提出了右值引用,作用是為了和左值引用區分開來,其作用是: 右值引用限制了其只能接收右值,可以利用這個特性從而提供重載,這是右值引用有且唯一的特性,限制了接收參數必為右值, 這點常用在move construct中,告訴別人這是一個即將消失的對象的引用,可以瓜分我的對象東西,除此之外,右值引用就沒有別的特性了。

class Base{
public:
      Base(const Base& b){...} //copy construct 
      Base(Base&& b){...}      //move construct
};

然后,一個右值引用變量在使用上就變成了左值,已經不再攜帶其是右引用這樣的信息,只是一個左值,這就是引用在c++中特殊而且復雜的一點,引用在c++中是一個特別的類型,因為它的值類型和變量類型不一樣, 左值/右值引用變量的值類型都是左值, 而不是左值引用或者右值引用

int val = 0;
int& val_left_ref = val;      
int&& val_right_ref = 0;

val_left_ref = 0;      // val_left_ref此時是int,而不是int&
val_right_ref = 0;     // val_right_ref此時是int, 而不是int&&

2.2 萬能引用(&&)

模板中的&&不代表右值引用,而是萬能引用,其既能接收左值又能接收右值。

template<typename T>
void emplace_back(T&& arg){

}

Class Base{

};

int main(){
    Base a;
    emplace_back(a);      // ok
    emplace_back(Base()); // also ok
return 0;
}

這種特性常用在容器元素的增加上,利用傳參是左值還是右值進而在生成元素的時候調用copy construct還是move construct,比如說vector的emplace_back。
見3.3的描述,不是所有的模板引用都是萬能引用,萬能引用只發生在推導時

template<typename T>
void f(T&& param);               // deduced parameter type ⇒ type deduction;
                                 // && ≡ universal reference
 
template<typename T>
class Widget {
    ...
    Widget(Widget&& rhs);        // fully specified parameter type ⇒ no type deduction;
    ...                          // && ≡ rvalue reference
};
 
template<typename T1>
class Gadget {
    ...
    template<typename T2>
    Gadget(T2&& rhs);            // deduced parameter type ⇒ type deduction;
    ...                          // && ≡ universal reference
};
 
void f(Widget&& param);          // fully specified parameter type ⇒ no type deduction;
                                 // && ≡ rvalue reference

2.3 為什么需要std::forward

模板的萬能引用只是提供了能夠接收同時接收左值引用和右值引用的能力,但是引用類型的唯一作用就是限制了接收的類型,后續使用中都退化成了左值,我們希望能夠在傳遞過程中保持它的左值或者右值的屬性, 如果不使用forward,直接按照下面的方式寫就會導致問題。

void RFn(int&& arg){

}

template<typename T>
void ProxyFn(T&& arg){
      RFn(arg);
}

void main(){
     ProxyFn(1);
}

會發現右值版本不能傳過去, [int]無法到[int&&],就導致參數不匹配。

為了解決這個問題,引入了std::forward, 將模板函數改成如下形式就可以了, forward被稱為完美轉發,語義上:數據是左值就轉發成左值,右值就轉發成右值,哪怕在萬能引用中也是如此

template<typename T>
void ProxyFn(T&& arg){
    RFn(std::forward<T>(arg));
}

2.4 forward的實現原理與細節

左值和右值引用在完成了參數傳遞之后,再使用時已經完全退化成了左值了,那么forward是如何實現完美轉發的呢,舉個例子:

Class Base{
public:
      Base(const Base& b){
            // copy construct
      }

      Base(Base&& b){
            //move construct
      }
};

template<typename T>
void ProxyFn(T&& arg){
      Base(std::forward<T>(arg));
}

void main(){
     Base b;
     ProxyFn(b);
}

整個推導轉發的過程如下圖,為了敘述方便,把forward的源碼也拷了過來:

圖中所說的T會被推導成Base&,是因為在萬能引用中,編譯器有一個規則,如果傳入的是左值,則模板類型會被推導成左值引用類型; 傳入的是右值,則模板類型就是值的類型

將T的實際類型代入后,會發現出現了Base& && arg這種類型,編譯器會將Base& &&轉成Base&,這個過程稱之為引用折疊, 引用折疊的規則如下, 簡而言之,只有左右兩個引用都為右值引用時才會折疊成右值引用

Base&& && -> Base&&
Base& &&  -> Base&
Base&& &  -> Base&
Base& &   -> Base& 

當然這個arg的類型我們實際上是不關心的,因為它的類型沒有什么作用了,在后面的使用下已經退化成左值了。

真正需要關注的是std::forward<T>(arg),其是實現完美轉發的關鍵,這個forward中的TBase& 給傳遞了過去, 然后在forward中_Ty&&進行折疊推導后,就變成了Base&,這就使得static_cast以及返回的類型都是Base&

這里有兩個很重要的點,返回的類型雖然是Base&,但前文不是說引用只是用來對接收參數的類型起限制作用,后續使用的時候就完全退化成了左值了嗎? forward的完美轉發對於返回的是Base&類型還好,但加入推導返回的是Base&&,傳遞到Base去構造的時候,不還是傳一個左值嗎,匹配到還是copy construct,無法達到完美轉發需求嗎?這一塊就牽涉到c++標准中的對左值右值的規定,詳情可見3.1,其中規定了返回值中, 左值引用的值類型是左值,右值引用的值類型是右值所以無論是forward也好,還是move也好,都是通過函數返回值來實現左值化和右值化的,一切都是為了這一步,最后上面的例子返回的是Base&, 因為它的值是左值,所以會匹配到copy construct。

其次,forward其實有兩個版本,但是上面的例子中只給出了一個,因為我們是在萬能引用的場景中使用std::forwared<>,因為傳遞的是左值,所以優先匹配是forward左值引用的版本, 正如其注釋所言,將一個左值轉發成一個左值或者右值,上面分析過這是由模板類型_Ty所決定的最終轉發成什么類型;當傳遞給forward的是一個右值的時候,才會去匹配第二個萬能引用版本。

// FUNCTION TEMPLATE forward
template <class _Ty>
_NODISCARD constexpr _Ty&& forward(
    remove_reference_t<_Ty>& _Arg) noexcept { // forward an lvalue as either an lvalue or an rvalue
    return static_cast<_Ty&&>(_Arg);
}

template <class _Ty>
_NODISCARD constexpr _Ty&& forward(remove_reference_t<_Ty>&& _Arg) noexcept { // forward an rvalue as an rvalue
    static_assert(!is_lvalue_reference_v<_Ty>, "bad forward call");
    return static_cast<_Ty&&>(_Arg);
}

下面的例子是forward配合萬能引用轉發右值的過程

2.總結

c++ 搞出了左值引用和右值引用是為了提供能夠根據接收左值/右值類型不同而產生重載,整個系統設計的也還算優雅,但是支整套引用系統的隱藏了不少細節,只是理解的時候需要了解這些東西,理解完后,抽象出引用的目的和核心就好了:

  • 2.1 左值引用只能接收左值,右值引用只能接收右值

  • 2.2 引用變量的值的類型是左值,而不是左值引用或右值引用。

template<typename T>
void fn(T arg){..}

main(){
   int temp;
   int&& a1 = 0;
   int& a2 = temp;    // 
   fn(a1);            // a1為int, 
   fn(a2);            // a2為int,
}

  • 2.3 右值引用有且只有一個特性:限制了接收的參數必為右值,可利用其提供函數重載; 這之后,變量的使用上就退換成左值,見1.2。

  • 2.4 forward的語義為: 數據是左值就轉發成左值,右值就轉發成右值,哪怕在萬能引用中也是如此; 使用場景為: 配合萬能引用實現完美轉發。

  • 2.5 forward和move的原理是: c++編譯器規定了函數返回的左值引用是左值,返回的右值引用是右值,通過這個特性配合static_cast的轉換,返回了左值/右值。

int& fn(){...} //返回類型為左值引用,但是返回值為左值
int&& fn(){...} //返回值為右值引用,但是返回值為右值
  • 2.6 模板的萬能引用是通過引用折疊實現,而且,左值傳遞到萬能引用上,模板類型會先被推導成左值引用以支持引用折疊推導, 右值不做處理,引用折疊推導后剛好是右值引用
template<typename T>
void fn(T&& arg){..}

main(){
   int a = 0;
   fn(a);             // T為int&, 見1.4
   fn(std::move(a));  // T為int, 見1.3
}
  • 2.7 const左值引用能夠接收一個右值,延長其生命周期,編譯器對該右值進行一個拷貝,使其生命周期和引用的保持一致

3.ref

3.1 Value categories
3.2 使用boost的type_index打印類型
3.3 萬能引用


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM