現代C++之右值語義

更多精彩內容,請關注微信公衆號:後端技術小屋ios

現代C++之右值語義

在現代C++的衆多特性中,右值語義(std::move和std::forward)大概是最神奇也最難懂的特性之一了。本文簡要介紹了現代C++中右值語義特性的原理和使用。redis

1 什麼是左值,什麼是右值?

int a = 0;       // a是左值,0是右值
int b = rand();  // b是左值,rand()是右值

直觀理解:左值在等號左邊,右值在等號右邊後端

深刻理解:左值有名稱,可根據左值獲取其內存地址,而右值沒有名稱,不能根據右值獲取地址。微信

2 引用疊加規則

左值引用A&和右值引用A&&可相互疊加, 疊加規則以下:分佈式

A& + A& = A&
A& + A&& = A&
A&& + A& = A&
A&& + A&& = A&&

舉例說明,在模板函數void foo(T&& x)中:函數

  • 若是Tint&類型, T&&int&x爲左值語義
  • 若是Tint&&類型, T&&int&&, x爲右值語義

也就是說,無論輸入參數x爲左值仍是右值,都能傳入函數foo。區別在於兩種狀況下,編譯器推導出模板參數T的類型不同。源碼分析

3 std::move

3.1 What?

在C++11中引入了std::move函數,用於實現移動語義。它用於將臨時變量(也有多是左值)的內容直接移動給被賦值的左值對象。this

3.2 Why?

知道了std::move是幹什麼的,他能給咱們的搬磚工做帶來哪些好處呢? 舉例說明:指針

若是類X包含一個指向某資源的指針,在左值語義下,類X的複製構造函數定義以下:rest

X::X() 
{
  // 申請資源(指針表示)
}

X::X(const X& other)
{
  // ...
  // 銷燬資源 
  // 克隆other中的資源
  // ...
}

X::~X() 
{
  // 銷燬資源
}

假設應用代碼以下。其中,對象tmp被賦給a以後,便再也不使用。

X tmp;
// ...通過一系列初始化...
X a = tmp;

在上面的代碼中,執行步驟:

  • 先執行一次默認構造函數(默認構造tmp對象)
  • 再執行一次複製構造函數(複製構造a對象)
  • 退出做用域時執行析構函數(析構tmp和a對象)

從資源的視角來看,上述代碼中共執行了2次資源申請和3次資源釋放。

那麼問題來了,既然對象tmp只是一個臨時對象,在執行X a = tmp;時,對象a可否將tmp的資源'偷'過來,直接爲我所用,而不影響原來的功能? 答案是能夠。

X::X(const X& other)
{
  // 使用std::swap交換this和other的資源        
}

經過'偷'對象tmp的資源,減小了資源申請和釋放的開銷。而std::swap交換指針代價極小,可忽略不計。

3.3 How?

到如今爲止,咱們明白了std::move將要達到的效果,那麼它到底是怎麼實現的呢?

template<class T> 
typename remove_reference<T>::type&&
std::move(T&& a) noexcept
{
  typedef typename remove_reference<T>::type&& RvalRef;
  return static_cast<RvalRef>(a);
}

無論輸入參數爲左值仍是右值,都被remove_reference去掉其引用屬性,RvalRef爲右值類型,最終返回類型爲右值引用。

3.4 Example

在實際使用中,通常將臨時變量做爲std::move的輸入參數,並將返回值傳入接受右值類型的函數中,方便其'偷取'臨時變量中的資源。須要注意的是,臨時變量被'偷'了以後,便不能對其進行讀寫,不然會產生未定義行爲。

#include <utility>             
#include <iostream>            
#include <string>              
#include <vector>              
                               
void foo(const std::string& n) 
{                              
  std::cout << "lvalue" << std::endl;
}                              
                               
void foo(std::string&& n)      
{                              
  std::cout << "rvalue" << std::endl;
}                              
                               
void bar()                     
{                              
  foo("hello");                // rvalue
  std::string a = "world";      
  foo(a);                      // lvalue
  foo(std::move(a));           // rvalue
}

int main()
{
  std::vector<std::string> a = {"hello", "world"};
  std::vector<std::string> b;

  b.push_back("hello");         // 開銷:string複製構造
  b.push_back(std::move(a[1])); // 開銷:string移動構造(將臨時變量a[1]中的指針偷過來)

  std::cout << "bsize: " << b.size() << std::endl;
  for (std::string& x: b)
    std::cout << x << std::endl;
  bar();
  return 0;
}

4 std::forward

4.1 What?

std::forward用於實現完美轉發。那麼什麼是完美轉發呢?完美轉發實現了參數在傳遞過程當中保持其值屬性的功能,即如果左值,則傳遞以後仍然是左值,如果右值,則傳遞以後仍然是右值。

簡單來講,std::move用於將左值或右值對象強轉成右值語義,而std::forward用於保持左值對象的左值語義和右值對象的右值語義。

4.2 Why?

#include <utility>
#include <iostream>

void bar(const int& x)
{
  std::cout << "lvalue" << std::endl;
}

void bar(int&& x)
{
  std::cout << "rvalue" << std::endl;
}

template <typename T>
void foo(T&& x)
{
  bar(x);
}

int main()
{
  int x = 10; 
  foo(x);  // 輸出:lvalue
  foo(10); // 輸出:lvalue
  return 0;
}

執行以上代碼會發現,foo(x)foo(10)都會輸出lvaluefoo(x)輸出lvalue能夠理解,由於x是左值嘛,可是10是右值,爲啥foo(10)也輸出lvalue呢?

這是由於10只是做爲函數foo的右值參數,可是在foo內部,10被帶入了形參x,而x是一個有名字的變量,即右值,所以foobar(x)仍是輸出lvalue

那麼問題來了,若是咱們想在foo函數內部保持x的右值語義,該怎麼作呢?std::forward便派上了用場。

只需改寫foo函數:

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

4.3 How?

std::forward聽起來有點神奇,那麼它究竟是如何實現的呢?

template<typename T, typename Arg> 
shared_ptr<T> factory(Arg&& arg)
{ 
  return shared_ptr<T>(new T(std::forward<Arg>(arg)));
}
template<class S>
S&& forward(typename remove_reference<S>::type& a) noexcept
{
  return static_cast<S&&>(a);
}

X x;
factory<A>(x);

若是factory的輸入參數是一個左值,那麼Arg = X&,根據疊加規則,std::forward<Arg> = X&。所以,在這種狀況下,std::forward<Arg>(arg)仍然是左值。

相反,若是factory輸入參數是一個右值,那麼Arg = Xstd::forward<Arg> = X。這種狀況下,std::forward<Arg>(arg)是一個右值。

剛好達到了保留左值or右值語義的效果!

4.4 Example

直接上代碼。若是前面都懂了,相信這段代碼的輸出結果也能猜個八九不離十了。  

#include <utility>
#include <iostream>

void overloaded(const int& x)
{
  std::cout << "[lvalue]" << std::endl;
}

void overloaded(int&& x)
{
  std::cout << "[rvalue]" << std::endl;
}

template <class T> void fn(T&& x)
{
  overloaded(x);
  overloaded(std::forward<T>(x));
}

int main()
{
  int i = 10; 
  overloaded(std::forward<int>(i));
  overloaded(std::forward<int&>(i));
  overloaded(std::forward<int&&>(i));
  
  fn(i);
  fn(std::move(i));

  return 0;
}

推薦閱讀

更多精彩內容,請掃碼關注微信公衆號:後端技術小屋。若是以爲文章對你有幫助的話,請多多分享、轉發、在看。
二維碼

相關文章
相關標籤/搜索