淺談std::move和std::forward原理

前言

本文主要整理了C++11中std::move和std::forward的原理, 這對理解C++的移動拷貝有很重的意義。

一、左值和右值

左值: 一般來說,能在內存中取得其地址, 即是左值。

右值:在內存在無取得其地址的, 即是右值。

note: 左值持久,右值暫短。 左值有持久的狀態,一般是變量, 而右值要麼是字面常量, 要麼是在表達式求值過程中創建的臨時對象。

二、左值引用和右值引用

右值引用:綁定到右值的引用。 (爲了支持移動操作引入)

左值引用: 爲了區分右值引用, 我們把常規的引用稱爲左值引用。

右值引用引入的意義:

Rvalue references is a small technical extension to the C++ language. Rvalue references allow programmers to avoid logically unnecessary copying and to provide perfect forwarding functions. They are primarily meant to aid in the design of higher performance and more robust libraries.

簡單說,引入右值引用就是爲了避免不必要的拷貝和支持完美轉發。

三、std::move

簡單瞭解了左值, 右值, 左值引用, 右值引用。

接下來描述std::move和std::forward的功能以及原理分析。

函數功能

std::move: 功能將一個左值/右值, 轉換爲右值引用。 主要是將左值強制轉爲右值引用,因爲右值引用無法直接綁定到左值上, 爲了能讓右值引用綁定到左值上, 必須將左值轉爲右值引用,std::move提供做的就是這個。 對於傳入右值, 那麼std::move將什麼都不做, 直接返回對應的右值引用。

std::forward: 功能將參數類型原封不打轉發到一下個函數, 包括const屬性。 這就是所謂的**“完美轉發(perfect forwarding)”**

在深入分析std::move和std::forward之前, 先了解一個概念**“引用摺疊”/ “引用坍塌”(reference-collapsing rules)**, 如果我們間接創建了一個引用的引用, 這些引用將會形成"摺疊", 規則如下。

  • X& & , X& && 和X&& &都會摺疊成類型X&
  • 類型X&& &&摺疊成X&&

由此可見, 只有一種情況摺疊成右值引用, 即右值引用的右值引用。 其他都摺疊爲左值引用。

std::move實現原理分析

std::move實現如下

  /**
   *  @brief  Convert a value to an rvalue.
   *  @param  __t  A thing of arbitrary type.
   *  @return The parameter cast to an rvalue-reference to allow moving it.
  */
  template<typename _Tp>
    constexpr typename std::remove_reference<_Tp>::type&&
    move(_Tp&& __t) noexcept
    { return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }

接着看看std::remove_reference<_Tp>的實現,可以看出,其主要是將去除類型的引用。 對於模板參數T, T&, T&&. 其type類型都爲T, 而且T如果有const屬性,則type也會保留.

  /// remove_reference
  template<typename _Tp>
    struct remove_reference
    { typedef _Tp   type; };

  template<typename _Tp>
    struct remove_reference<_Tp&>
    { typedef _Tp   type; };

  template<typename _Tp>
    struct remove_reference<_Tp&&>
    { typedef _Tp   type; };

std::move函數類型爲Tp_&&, 一個指向模板類型參數的右值引用, 通過引用摺疊,此參數可以與任意類型匹配。

函數的返回值爲std::remove_reference<_Tp>::type&&,std::remove_reference<Tp>::type類型爲Tp, 因此,返回類型Tp&&.

以下分析根據具體事例分析。(實例來自《C++primer fifth》 16.2.6 理解std::move)

sting s1("hi!"), s2;
s2 = std::move(string("bye!"));  // 傳入一個右值
s2 = std::move(s1);              // 傳入一個左值

在s2 = std::move(string(“bye!”)); 傳入的是一個右值。因此,在std::move模板函數中,

  • 推斷出的T類型爲string
  • 因此, std::remove_reference用string進行實例化
  • std::remove_reference的type成員是string
  • move的返回類型是string&&
  • move的參數__t類型爲string&&.

因此std::move最終被實例化如下。 __t類型已經是string&&, 因此類型轉換什麼都不用做。 即對於傳入右值的std::move函數, 實際上move函數什麼都不用做。

string&& move(string&& __t){
	return static_cast<string &&>(__t);
}

在s2 = std::move(s1); 傳入的是一個左值,因此, 在std::move模板函數中,

  • 推斷出T的類型爲string& ( string的引用, 而不是普通的string, 這個是特殊規則)。
  • 因此,std::remove_reference用string& 進行實例化
  • std::remove_reference的type成員是string
  • move的返回類型是string&&
  • move的參數__t類型爲string& &&, 會被摺疊爲string&.

因此std::move最終被實例化如下, __t類型是String&, 因此cast將__t類型string&轉爲string&&.

string&& moe(string& __t){
	return static_cast<string &&>(__t);
}

通過以上分析, 知道std::move, 無論傳遞左值/右值, 我們都可以獲取到一個右值引用。 傳遞左值的時候,函數會使用cast進行強制轉化, 傳遞右值,則什麼都不用做。

四、std::forward

首先看看std::forward實現, forward提供兩個重載版本, 一個針對左值, 一個針對右值。

  /**
   *  @brief  Forward an lvalue.
   *  @return The parameter cast to the specified type.
   *
   *  This function is used to implement "perfect forwarding".
   */
  template<typename _Tp>
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type& __t) noexcept
    { return static_cast<_Tp&&>(__t); }

  /**
   *  @brief  Forward an rvalue.
   *  @return The parameter cast to the specified type.
   *
   *  This function is used to implement "perfect forwarding".
   */
  template<typename _Tp>
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
    {
      static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
		    " substituting _Tp is an lvalue reference type");
      return static_cast<_Tp&&>(__t);
    }

根據以下實例進行分析, 實例來自 知乎 ,網名藍色的回答。

template<typename T>
void foo(T&& fparam)
{
    std::forward<T>(fparam);
}

int i = 7;
foo(i);
foo(47);

在foo(i), 如果傳入的是一個左值, 那麼foo中T的類型將是int&, fparam類型是int& &&, 經過摺疊爲int&. 因此,在std::forward模板函數中,

  • 推斷出T的類型爲int&,
  • 因此, std::remove_reference用int& 進行實例化
  • std::remove_reference的type成員是int
  • forward返回類型爲int& &&, 摺疊爲int&
  • forward的參數類型__t爲int&
  • static_cast<int & &&> 摺疊爲static_cast<int &>

因此std::forward最終被實例化如下。因此可以發現,函數什麼都不用做, 最終的傳入forward的左值引用被保留了。

int &forward(int &__t){
	return static_cast<int &>(__t)
}

在foo(47)中, 傳入的是一個右值,那麼foo中T的類型將是int, fparam類型是T&&, 因此,在std::forward模板函數中

  • 推斷出T的類型爲int
  • 因此, std::remove_reference用int 進行實例化
  • std::remove_reference的type成員是int
  • forward返回類型爲int&&
  • forward的參數類型__t爲int&&
  • static_cast<int &&>

因此std::forward最終被實例化如下。因此可以發現,函數什麼都不用做, 最終的傳入forward的右值引用被保留了。

int &&forward(int &&__t){
	return static_cast<int &&>(__t)
}

通過以上分析, 實際上無論傳遞左值還是右值, forward都可以完美轉發, 並且函數內部什麼都不用做(轉發的屬性還包括const, 儘管例子沒有體現出來。)

五、參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章