C++左值引用與右值引用

adfas發表於2020-07-11

本文翻譯自:https://docs.microsoft.com/en-us/cpp/cpp/references-cpp?view=vs-2019

  引用,類似於指標,用於儲存一個位於記憶體某處的物件的地址。與指標不同的是,引用在被初始化後不能再指向另一個物件,或設定為null。引用分為兩種:左值引用,右值引用,其中左值引用指向一個命名的變數,右值引用指向一個臨時物件(temporary object)。操作符&表示左值引用,而&&根據其上下文的不同可表示右值引用或a universal reference。

  注:universal reference:https://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers

左值引用

作用:持有一個物件的地址,但是其行為類似於一個物件  

格式:type-id & cast-expression

  我們可以將左值引用看為一個物件的別名。左值引用的宣告包含一個可選的說明符列表,後面跟一個引用宣告符。左值引用必須被初始化,且不能再指向另外一個物件或設定為null。

  任何可以將其地址轉換為一個指定型別指標的物件也可將其地址轉換為一個類似的引用物件。例如,任何可以轉換為char*的物件的地址也可以轉換為char &。

  注意不要將引用宣告符(&)與地址操作符(取物件的地址,也是&)混淆。當&前面是一個型別時,例如int或char,則該&是一個引用宣告符。如果&前面沒有任何型別,則該&用於取一個物件的地址。

  • 例子:

   The following example demonstrates the reference declarator by declaring a Person object and a reference to that object. Because rFriend is a reference to myFriend, updating either variable changes the same object.

// reference_declarator.cpp
// compile with: /EHsc
// Demonstrates the reference declarator.
#include <iostream>
using namespace std;

struct Person
{
    char* Name;
    short Age;
};

int main()
{
   // Declare a Person object.
   Person myFriend;

   // Declare a reference to the Person object.
   Person& rFriend = myFriend;

   // Set the fields of the Person object.
   // Updating either variable changes the same object.
   myFriend.Name = "Bill";
   rFriend.Age = 40;

   // Print the fields of the Person object to the console.
   cout << rFriend.Name << " is " << myFriend.Age << endl;
}

  輸出:

Bill is 40

右值引用

  右值引用指向臨時物件。在C++11之前,可以通過一個左值引用指向一個臨時物件,但是這個左值引用必須是const:

string getName()
{
    return “Alex”;
}
const string& name = getName();

       這也表明,臨時物件並不是立即被銷燬(destructed),這由C++保證,但它仍然是一個臨時物件,你不能修改它的值。

       在C++11中,引入了右值引入,可以通過一個可變引用指向rvalue,但是不能繫結到lvalue,因此右值引用可以檢測一個值是否是臨時物件。

       類似於左值引用使用&,右值引用使用&&,可以是const/non-const

 

  持有一個指向右值表示式的引用。

  •   格式:type-id && cast-expression

  右值引用可以用於區分一個表示式是左值還是右值。左值引用與右值引用在句法上語法上類似,但是遵從不同的規則。下面章節用於描述右值引用是如何支援移動語義(move semantics)和完美轉發(perfect forwarding)的實現的。

Move Semantics

  移動語義(move semantics)的實現依賴於右值引用,move可以明顯地提升應用的效能。移動語義讓你可以通過程式碼將資源(例如動態分配的記憶體)從一個物件轉移到另一個物件。移動語義可以工作的原理是:它可以將資源從臨時物件(temporary objects)中轉移到其他地方,這些臨時變數在程式的其他地方不會被獲取到(be referenced)。

  為了實現移動語義,你需要給自定義的類提供:移動建構函式(move constructor),另外可選地提供移動賦值構造符(move assignment operator, operator=)。(如果實現了這些函式),持有右值資源的物件的拷貝和賦值操作符會自動使用move semantics。與預設的拷貝建構函式不同的是,編譯器並不會提供一個預設的移動建構函式。移動建構函式:https://docs.microsoft.com/en-us/cpp/cpp/move-constructors-and-move-assignment-operators-cpp?view=vs-2019

  你也可以過載普通函式和操作符來使用move semantics。Visual Studio 2010將move semantics引入到C++標準庫中。例如:string類實現了使用move semantics的相關操作。例如:

// string_concatenation.cpp
// compile with: /EHsc
#include <iostream>
#include <string>
using namespace std;

int main()
{
   string s = string("h") + "e" + "ll" + "o";
   cout << s << endl;
}

  在Visual Studio 2010之前,string的每一個+操作符都會分配並返回一個新的臨時的string物件(an rvalue)。+操作符並不能將一個string擴充套件到另一個string上,因為它不知道the source string是左值還是右值。如果+左右的兩個字串都是左值,它們可能在程式的其他地方被引用,因此不能修改。通過使用右值引用,可以修改+操作符用於右值,因為右值不會在程式的其他地方被引用。這就可以明顯地降低string類必須的動態記憶體分配。

  當編譯器不能使用Reture Value Optimization(RVO),或Named Return Value Optimization時,移動語義也能提升程式效能。在這種情況下,如果返回型別定義了move constructor的話編譯器會呼叫它。

  為了更好地理解移動語義,考慮向vector物件中插入元素這樣的一個例子。如果vector物件的容量超出了,vector物件為其儲存的元素需要重定位記憶體,然後將每個元素拷貝到另一個記憶體地址,以為新插入的元素騰出空間。當插入操作拷貝一個元素時,他會建立一個新的元素,呼叫拷貝建構函式以將之前的資料拷貝到新元素中,然後再將之前的元素析構掉(destroy)。而移動語義讓你可以直接地將元素進行移動,而不需要進行昂貴的記憶體重定位,和複製操作。為了使用move semantics,你可以寫一個move建構函式,用於將資料從一個物件移動到另一個。

Perfect Fowarding

  翻譯為:完美轉發??? C++11中的一項新技術,

https://blog.csdn.net/u012198575/article/details/83142419

https://docs.microsoft.com/en-us/cpp/cpp/rvalue-reference-declarator-amp-amp?view=vs-2019

       Perfect forwarding用於降低過載函式的需求,並且當你編寫一個引數為引用的泛型函式時,且該泛型函式將引數傳遞(或:forward)給其他函式時,有助於解決forwarding problem。例如:如果泛型函式的引數型別為const T&,那麼對該函式的呼叫不能修改引數的值。如果泛型函式的引數型別為T&,那麼不同使用rvalue(例如:臨時物件或整數字面常數:temporary object or integer literal)對該函式進行呼叫。

       對於該類問題的通常解決方法是對該泛型函式進行過載,對於每個引數都過載T& 和const T&的版本。但是,如果函式引數較多的話過載版本會指數型增加。

       右值引用rvalue reference可以讓你通過一個版本的函式來接收這兩種函式,且可通過forward將這些引數傳遞給其他函式。

       例如:對於4中型別:W X Y Z,它們的建構函式分別使用不同的const non-const左值引用組合:

struct W
{
   W(int&, int&) {}
};

struct X
{
   X(const int&, int&) {}
};

struct Y
{
   Y(int&, const int&) {}
};

struct Z
{
   Z(const int&, const int&) {}
};

  假如你要編寫一個泛型函式來生成這些物件,可以寫成下面形式:

template <typename T, typename A1, typename A2>
T* factory(A1& a1, A2& a2)
{
   return new T(a1, a2);
}

  通過如下形式可以呼叫該泛型函式:

int a = 4, b = 5;
W* pw = factory<W>(a, b);

  但是通過如下形式呼叫會產生錯誤,因為引數是rvalue,而泛型函式接受的引數是可修改的左值引用(lvalue references that are modifiable

Z* pz = factory<Z>(2, 2);

  通常解決這類問題的方法是過載:每個引數都有一個A& ,const A&的變化。右值引用則可以讓你只寫一個版本的函式:

template <typename T, typename A1, typename A2>
T* factory(A1&& a1, A2&& a2)
{
   return new T(std::forward<A1>(a1), std::forward<A2>(a2));
}

這個版本的泛型函式使用右值引用rvalue reference作為factory函式的引數。std::forward函式的作用是forward the parameters of the factory to the constructor of the template class,是將factory函式的引數轉發給模板類的建構函式。

 

該版本可以支援如下的呼叫方式:

int main()
{
   int a = 4, b = 5;
   W* pw = factory<W>(a, b);
   X* px = factory<X>(2, b);
   Y* py = factory<Y>(a, 2);
   Z* pz = factory<Z>(2, 2);

   delete pw;
   delete px;
   delete py;
   delete pz;
}

Additional Properties of Rvalue Reference右值引用的其他特性

   可以過載一個函式,讓它分別接受左值引用和右值引用。

  通過過載一個函式,讓它分別接受const左值引用和右值引用,你可以通過程式碼來判斷一個表示式是non-modifiable objects(lvalues)還是modifiable tempoary values(rvalues)。只有當一個物件被標記為const,你才能將它傳遞給一個引數為右值引用的函式。The following example shows the function f, which is overloaded to take an lvalue reference and an rvalue reference. The main function calls f with both lvalues and an rvalue.

// reference-overload.cpp
// Compile with: /EHsc
#include <iostream>
using namespace std;

// A class that contains a memory resource.
class MemoryBlock
{
   // TODO: Add resources for the class here.
};

void f(const MemoryBlock&)
{
   cout << "In f(const MemoryBlock&). This version cannot modify the parameter." << endl;
}

void f(MemoryBlock&&)
{
   cout << "In f(MemoryBlock&&). This version can modify the parameter." << endl;
}

int main()
{
   MemoryBlock block;
   f(block);
   f(MemoryBlock());
}

該例子輸出結果如下:

In f(const MemoryBlock&). This version cannot modify the parameter.
In f(MemoryBlock&&). This version can modify the parameter.

  在該例子中,第一個呼叫f函式傳遞了一個區域性變數(右值)作為引數。第二個呼叫f函式傳遞了一個臨時物件作為引數。由於臨時物件不能在程式的其他地方引用,因此呼叫過載函式中引數為右值的版本,且該版本中過對於傳入引數是可以修改的。

  注意:編譯器將一個命名的右值引用視為左值進行處理,並將一個未命名的右值引用視為右值進行處理。The compiler treats a named rvalue reference as an lvalue and an unnamed rvalue reference as an rvalue.

  當你編寫一個引數為右值引用的函式時,在函式體內該引數被視為是左值。編譯器將命名的右值引用當做一個左值進行處理,這是因為一個明明的物件可以在程式的多處被引用;但是允許程式的多處來修改或刪除資源時很危險的。例如:如果程式的多個位置都嘗試從一個物件中轉移資源,僅僅會有一個物件能成功地轉移該資源(source)。

  下面例子定義了一個函式g,該函式過載了一個左值引用和右值引用作為引數。函式f的引數為左值引用(一個明明的右值引用),並返回一個右值引用(一個未命名的右值引用)。在f函式中呼叫g,overload resolution選擇了引數為左值引用版本的函式g,因為f的函式體將其引數視為左值。在main函式中呼叫g,overload resolution選擇了引數為優質引用版本的函式g,因為f函式返回的是一個右值引用。

// named-reference.cpp
// Compile with: /EHsc
#include <iostream>
using namespace std;

// A class that contains a memory resource.
class MemoryBlock
{
   // TODO: Add resources for the class here.
};

void g(const MemoryBlock&)
{
   cout << "In g(const MemoryBlock&)." << endl;
}

void g(MemoryBlock&&)
{
   cout << "In g(MemoryBlock&&)." << endl;
}

MemoryBlock&& f(MemoryBlock&& block)
{
   g(block);
   return move(block);
}

int main()
{
   g(f(MemoryBlock()));
}

上面例子的輸出為:

In g(const MemoryBlock&).
In g(MemoryBlock&&).

  在該例子中,mian函式傳遞一個右值給f函式。f函式體將它的引數當做是左值進行處理,函式f呼叫g時呼叫的是g的左值引用的版本,因此第一個列印的是左值引用版本中的內容。

  •   你可以將一個左值轉換為右值引用

   C++標準庫函式std::move允許你將一個物件轉換為該物件的右值引用。另外,你可以使用static_cast關鍵字將一個左值轉換為右值引用,看下面的例子:

// cast-reference.cpp
// Compile with: /EHsc
#include <iostream>
using namespace std;

// A class that contains a memory resource.
class MemoryBlock
{
   // TODO: Add resources for the class here.
};

void g(const MemoryBlock&)
{
   cout << "In g(const MemoryBlock&)." << endl;
}

void g(MemoryBlock&&)
{
   cout << "In g(MemoryBlock&&)." << endl;
}

int main()
{
   MemoryBlock block;
   g(block);
   g(static_cast<MemoryBlock&&>(block));
}

輸出:

In g(const MemoryBlock&).
In g(MemoryBlock&&).
  • 函式模板推斷它們的模板引數型別,然後使用引用摺疊規則。Function template deduce their template argument types and then use reference collapsing rules.

   編寫函式模板然後將其引數再傳遞給其他函式的行為很常見。理解引數為右值引用的函式模板的模板型別推斷(template type deduction)的工作原因很重要。

  如果函式引數是一個右值,編譯器會推斷該引數為一個右值引用。例如,如果你傳遞一個型別為x的右值引用給一個引數為T&&的模板函式,模板引數推斷(template argument deduction)會將T推斷為X。因此,引數型別為X&&。如果函式引數是一個左值或常量左值(const lvalue),則編譯器會將型別推斷為該型別的左值引用或const左值引用。

  下面例子中宣告瞭一個結構體模板,然後將它例項化為各個型別。print_type_and_value函式的引數為右值引用,然後forward該引數給合適的例項化版本的S::print函式。main函式展示了呼叫S::print函式的多種方式。

// template-type-deduction.cpp
// Compile with: /EHsc
#include <iostream>
#include <string>
using namespace std;

template<typename T> struct S;

// The following structures specialize S by
// lvalue reference (T&), const lvalue reference (const T&),
// rvalue reference (T&&), and const rvalue reference (const T&&).
// Each structure provides a print method that prints the type of
// the structure and its parameter.

template<typename T> struct S<T&> {
   static void print(T& t)
   {
      cout << "print<T&>: " << t << endl;
   }
};

template<typename T> struct S<const T&> {
   static void print(const T& t)
   {
      cout << "print<const T&>: " << t << endl;
   }
};

template<typename T> struct S<T&&> {
   static void print(T&& t)
   {
      cout << "print<T&&>: " << t << endl;
   }
};

template<typename T> struct S<const T&&> {
   static void print(const T&& t)
   {
      cout << "print<const T&&>: " << t << endl;
   }
};

// This function forwards its parameter to a specialized
// version of the S type.
template <typename T> void print_type_and_value(T&& t)
{
   S<T&&>::print(std::forward<T>(t));
}

// This function returns the constant string "fourth".
const string fourth() { return string("fourth"); }

int main()
{
   // The following call resolves to:
   // print_type_and_value<string&>(string& && t)
   // Which collapses to:
   // print_type_and_value<string&>(string& t)
   string s1("first");
   print_type_and_value(s1);

   // The following call resolves to:
   // print_type_and_value<const string&>(const string& && t)
   // Which collapses to:
   // print_type_and_value<const string&>(const string& t)
   const string s2("second");
   print_type_and_value(s2);

   // The following call resolves to:
   // print_type_and_value<string&&>(string&& t)
   print_type_and_value(string("third"));

   // The following call resolves to:
   // print_type_and_value<const string&&>(const string&& t)
   print_type_and_value(fourth());
}

上述例子輸出為:

print<T&>: first
print<const T&>: second
print<T&&>: third
print<const T&&>: fourth

  為解析每次對函式print_type_and_value函式的呼叫,編譯器首先進行模板變數推斷。當編譯器將推導的模板引數替換為引數型別時,編譯器運用引用摺疊規則(reference collapsing rule)。例如,將區域性變數s1傳遞給print_type_and_value函式時,編譯器會產生如下的函式簽名:

print_type_and_value<string&>(string& && t)

  編譯器使用引用摺疊規則將簽名減少到以下內容:

print_type_and_value<string&>(string& t)

  這個版本的print_type_and_value函式然後forward(轉發)它的引數到正確版本的S::print函式。

  下面表格總結了模板變數型別推斷的引用摺疊規則:

 

Expanded type Collapsed type
T& & T&
T& && T&
T&& & T&
T&& && T&&

   模板變數型別推斷是實現perfect forwarding(完美轉發)的重要組成部分。

 總結

   右值引用可以用於區分左值,右值。右值引用可以通過消除不必要的記憶體分配和copy行為來提升你的程式的效能。右值引用還可以讓你只寫一個版本的函式,就可以接受不同的引數並forward轉發該引數到其他函式,就像其他函式被直接呼叫一樣。They also enable you to write one version of a function that accepts arbitrary arguments and forwards them to another function as if the other function had been called directly.

相關文章