https://codinfox.github.io/dev/2014/06/03/move-semantic-perfect-forward/
有名字,能用取址符號取地址的就是左值
std::move()無條件地將左值轉換成右值
std::forward<T>(xxx) 保留xxx的左右值屬性, 是左值還是右值,取決於模板參數T的實際類型,是int, int&, int&&, 然后對T &&應用折疊規則。
僅當發生自動類型推導時(模板編程,auto 關鍵字),T&&
才是未定引用類型
新標准重新定義了lvalue和rvalue,並允許函數依照這兩種不同的類型進行重載。
通過對於右值(rvalue)的重新定義,語言實現了移動語義(move semantic)和完美轉發(perfect forwarding),通過這種方法,C++實現了在保留原有的語法並不改動已存在的代碼的基礎上提升代碼性能的目的。
作為一個C++0x標准的初學者,理解這些概念其實還有有一定的困難的,加上網上能夠找得到的中文資源有比較的少,少有的資源寫的也都不是那么的通俗易懂,多少有點晦澀,這也為學習設置了一定的障礙。同樣作為初學者,我就花了不少時間研究這些概念,終於算是有所體悟。這里就把我粗淺的理解記錄於此,希望能夠給后來接觸這些內容的同儕們以幫助,也供我日后參考。謬誤之處在所難免,還望不吝賜教。
移動語義解決了什么問題
我們先看一段代碼。
class Test { int * arr{nullptr}; //c11的大括號初始化語法: https://www.cnblogs.com/my_life/articles/7909797.html public: Test():arr(new int[5000]{1,2,3,4}) { cout << "default constructor" << endl; } Test(const Test & t) { cout << "copy constructor" << endl; if (arr == nullptr) arr = new int[5000]; memcpy(arr, t.arr, 5000*sizeof(int)); } ~Test(){ cout << "destructor" << endl; delete [] arr; } };
這是一段常見的類的定義。在其中我們定義了一個int
類型數組arr
,它一共有5000個元素。考慮到我們可以使用一個已有的Test
對象來初始化一個新的Test
對象,我們實現了復制構造函數(copy constructor)。
接下來,我們考慮一個這樣的應用場景:
int main() { Test reusable; // do something to reusable Test duplicated(reusable); // do something to reusable }
我們創建了一個reusable
變量並對其做了某一些操作,之后我們使用這個更改過的reusable
變量初始化一個duplicated
變量,在對其進行初始化之后,我們依然需要對reusable
做其他的操作。
在這個情境下,reusable
和duplicated
變量各自有自己的用處,沒有誰是為誰附帶產生的。所以我們看到,在這個情境下,我們的復制構造函數是合情合理的。
現在我們考慮另外一個場景:
Test createTest() { return Test(); } int main() { Test t(createTest()); }
在這個場景當中,我們需要使用一個工廠函數來構造Test
的實例。那么在這個場景下,我們的復制構造函數被調用了2次。這兩次調用相當於復制了10000個元素,是一個不小的開銷。可是我們的這個開銷有意義嗎?
我們知道,在工廠函數當中建立的Test
實例在函數返回時就會被析構,而用於返回值的Test
的臨時實例也會在將值賦給main
函數當中的t
之后被析構。
也就是說,這兩個臨時對象事實上並沒有什么意義。由於構造他們而產生的復制的開銷其實完全沒有必要(事實上,編譯器一般會對這種情況進行(N)RVO,但不見得每次都能很好的優化)。
所以我們就在考慮,有沒有可能我們可以將在工廠函數當中構造的成員變量的那塊內存“偷”過來,而不是重新開辟一塊內存,然后再將之前的內容復制過來呢?
移動語義(move semantic)
鐺鐺鐺鐺!移動語義登場了!移動語義就是為了解決上面的這種問題而產生的。通過移動語義,我們可以在沒有必要的時候避免復制。那么在接下來,我們就重點來談一談移動構造函數(move constructor)。
相信到這里你已經意識到了,移動構造函數的出現就是為了解決復制構造函數的這個弊病。所以,其實移動構造函數應該和復制構造函數實現差不多的功能。
那么,它也應該是一種構造函數的重載(好廢的廢話……)。所以,我們可以想象出來,其實移動構造函數大概就會是這個樣子:
Test(<KEYWORD> t):arr(t.arr){t.arr = nullptr;}
這里解釋一下,通過移動構造函數,事實上我們是做了一個淺拷貝(shallow copy)。
至於要將之前的指針置為空的原因在於,我們的類會在析構的時候delete掉我們的數組。那么我們淺拷貝出來的這個對象的成員變量(arr
指針)就變成了一個懸掛指針(dangling pointer)。
好了,現在的問題變成了,這個<KEYWORD>
究竟是什么?編譯器如何自動判斷到底應該調用復制構造函數(我突然想起來這個東西的翻譯貌似應該是拷貝構造函數,但是既然都已經寫了這么多了,我就不改了)還是移動構造函數呢?
左值(lvalue)、右值(rvalue)、左值引用(lvalue reference)和右值引用(rvalue reference)
左值和右值
為了回答上面的這個問題,我們首先需要明確左值和右值的概念。C++定義了與C不相同的左值和右值的判斷方法,不過說起來非常簡單:凡是真正的存在內存當中,而不是寄存器當中的值就是左值,其余的都是右值。
其實更通俗一點的說法就是:凡是取地址(&
)操作可以成功的都是左值,其余都是右值。現在相信大家都已經知道左值和右值的關系了。我們來看幾個例子:
// lvalues: int i = 42; i = 43; // ok, i is an lvalue int* p = &i; // ok, i is an lvalue int& foo(); foo() = 42; // ok, foo() is an lvalue int* p1 = &foo(); // ok, foo() is an lvalue // rvalues: int foobar(); int j = 0; j = foobar(); // ok, foobar() is an rvalue,返回的是臨時變量,臨時變量不能用取址操作符 int k = j + 2; // ok, j+2 is an rvalue int* p2 = &foobar(); // error, cannot take the address of an rvalue j = 42; // ok, 42 is an rvalue
那么,函數是不是就只可以作為右值呢?其實不是。考慮一個我們司空見慣的例子:
vector<int> vec = {1,2,3,4,5}; vec[1] = 99; // overloaded operator[]
我們看到,其實operator[]
是一個函數,其返回值依然可以作為左值。
左值引用和右值引用
好了,在明確了左值和右值的關系之后,左值引用而右值引用的概念也就顯而易見了。對於左值的引用就是左值引用,而對於右值的引用就是右值引用。雖然這么說,但是其實這個概念還並不是那么好理解。
事實上,不好理解的原因是我們之前從來沒有真正的去區分過這兩個概念,因為我們曾經將左值引用直接稱為“引用”。也就是說,我們曾經一直用的int&
事實上是對於int
類型左值的引用。而對於右值呢?在新標准當中我們使用int&&
來表示。我們不妨看看幾個例子:
void foo(const int & i) { cout << "const int & " << i << endl; } void foo(int & i) { cout << "int & " << i << endl; } void foo(int && i) { cout << "int && " << i << endl; } void foo(const int && i) { cout << "const int && " << i << endl; } // 這是個奇葩,我一會說
我們在以往使用的時候大多會使用第一種形式。其實,第一種形式是一種神奇的形式,因為const int &
既可以綁定左值,也可以綁定右值。所以在沒有后面三個重載函數的情況下,我們調用一下語句:
int i = 2; foo(i); foo(2);
他們的輸出都是const int & 2
。
而如果在只有第二個函數而沒有其他函數的時候,第三條語句是違法的。
在只有第三個函數沒有其它函數的時候,第一條語句是違法的(函數需要傳遞一個右值,而第一條語句,i是個左值:cannot bind ‘int’ lvalue to ‘int&&’)。
所以我們總結一下:const reference可以綁定所有的值,而其他類型的引用只能綁定自己類型的值。在這四種函數都存在的情況下,每一種函數都會綁定與自己最接近的那個值。
也就是說,在四個函數都存在的情況下,當我們再次運行上面的這段代碼,輸出的結果就將變成:
int & 2
int && 2
所以,當我們運行下面的語句:
foo(i); foo(j); foo(2); foo([]()->const int && {return 2;}());
我們得到的結果將會是:
int & 2
const int & 2
int && 2
const int && 2
這里解釋一下第四個。第四條語句編譯的時候會有Warning,提示”Returning reference to local temporary object”。想想也確實是這么個事情,不過它讓我通過了,而且結果沒錯誤。我覺得這個是不靠譜的。不過其實仔細考慮一下,常量右值引用其實不太能想出什么應用場景。所以個人認為,這只是貫徹C++標准當中”不應當組織程序員拿起槍射自己的腳“的精神,到不一定有什么實際意義,所以這個就不要糾結了。
相信現在大家已經能夠對於左值、右值、左值引用和右值引用有一個准確的認識了。
回到之前的問題
現在我們可以知道上面那個Test
類當中的神奇的<KEYWORD>
到底是什么了。其實他就是Test &&
。由於左值和右值是兩種不同的類型,所以可以依照這個類型進行重載。
所以我們的Test
類就變成了這樣:
class Test { int * arr{nullptr}; public: Test():arr(new int[5000]{1,2,3,4}) { cout << "default constructor" << endl; } Test(const Test & t) { cout << "copy constructor" << endl; if (arr == nullptr) arr = new int[5000]; memcpy(arr, t.arr, 5000*sizeof(int)); } Test(Test && t): arr(t.arr) { //移動構造函數 cout << "move constructor" << endl; t.arr = nullptr; } ~Test(){ cout << "destructor" << endl; delete [] arr; } };
所以,當我們再次考慮下面這個應用場景的時候:
Test createTest() { //返回的是一個臨時變量,是右值 return Test(); //return,第一次調用move construct } int main() { Test t(createTest()); //createTest()返回一個右值,第二次調用move constructor }
我們會發現,打印的結果變成了:
default constructor
move constructor
destructor
move constructor
destructor
destructor
也就是說,我們的Test
實例在工廠函數當中被使用默認構造函數(default constructor)構造一次之后,調用的全部都是移動構造函數,因為我們發現其實所有的這些值都是右值。這極大地節省了開支。
這里有一個編譯器的trick。gcc是一個喪心病狂的編譯器,他會強制進行(N)RVO。如果你不做任何設置直接用GCC編譯運行上面的代碼,你將看到的是:
default constructor
這個時候不要懷疑我上面說的東西有問題或者你寫錯了。請直接在gcc后面添加編譯參數
-fno-elide-constructors
。所以整個的編譯語句應該是:g++ -std=c++11 -fno-elide-constructors test.cpp # for instance
移動語義再多說幾句
現在我們再來看看一開始那個reusable
的例子。
int main() { Test reusable; // do something to reusable Test duplicated(reusable); // do something to reusable }
如果現在我們不想復制reusable
了,我們也想在構造duplicated
的時候使用轉移構造函數,那么應該怎么做呢?新標准給我們提供了一個解決方案:
Test duplicated(std::move(reusable)); //如果不用move將其轉換成右值,那么將還是調用的拷貝構造函數
這個std::move()
的作用是將左值轉換為右值。
不過這里要注意的一點是,如果我們在這里使用了move
的話,那么后面我們就不能再對reusable
進行操作了。
因為轉移構造函數已經將reusable
的成員變量arr
指針置為空了(見上面的Test的移動構造函數的實現)。
講解完了轉移構造函數,其實轉移賦值語句(move assignment)與之同理,各位就自己研究一下吧。由於STL已經默認對所有的代碼進行了右值引用的改寫,所以現在當你運行你之前寫過的代碼時,你不需要做任何的更改,就會發現似乎更快了一些。
進一步探討左值和右值
我們來考慮下面的情景:
void doWork(TYPE&& param) { // ops and expressions using std::move(param) }
這個代碼是從Scott Meyers的演講當中摘取的。現在的問題是:** param
是右值嗎? **答案是:不!param
是一個左值。
這里牽扯到一個概念,即事實上左值和右值與類型是沒有關系的,即int
既可以是左值,也可以是右值。
區別左值和右值的唯一方法就是其定義,即能否取到地址。在這里,我們明顯可以對param
進行取地址操作,所以它是一個左值。
也就是說,但凡有名字的“右值”,其實都是左值。
這也就是為什么上面的代碼當中鼓勵大家對所有的變量使用std::move()
轉成右值的原因。
完美轉發(perfect forward)又是在做什么
我們依然考慮一個例子:
template <typename T> void func(T t) { cout << "in func" << endl; } template <typename T> void relay(T&& t) { cout << "in relay" << endl; func(t); } int main() { relay(Test()); //Test()沒名字的臨時變量,右值 }
在這個例子當中,我們的期待是,我們在main
當中調用relay
,Test
的臨時對象作為一個右值傳入relay
,在relay
當中又被轉發給了func
,那這時候轉發給func
的參數t
也應當是一個右值。
也就是說,我們希望:當relay
的參數是右值的時候,func
的參數也是右值;當relay
的參數是左值的時候,func
的參數也是左值。
那么現在我們來運行一下這個程序,我們會看到,結果與我們預想的似乎並不相同:
default constructor
in relay
copy constructor
in func
destructor
destructor
我們看到,在relay
當中轉發的時候,調用了復制構造函數,也就是說編譯器認為這個參數t
並不是一個右值,而是左值。這個的原因已經在上一節解釋過了,因為它有一個名字。
那么如果我們想要實現我們所說的,如果傳進來的參數是一個左值,則將它作為左值轉發給下一個函數;如果它是右值,則將其作為右值轉發給下一個函數,我們應該怎么做呢?
這時,我們需要std::forward<T>()
。
與std::move()
相區別的是,move()
會無條件的將一個參數轉換成右值,而forward()
則會保留參數的左右值類型。所以我們的代碼應該是這樣:
template <typename T> void func(T t) { cout << "in func " << endl; } template <typename T> void relay(T&& t) { cout << "in relay " << endl; func(std::forward<T>(t)); }
現在運行的結果就成為了:
default constructor
in relay
move constructor in func
destructor
destructor
而如果我們的調用方法變成:
int main() { Test t; //有名字,左值 relay(t); }
那么輸出就會變成:
default constructor
in relay
copy constructor
in func
destructor
destructor
完美地實現了我們所要的轉發效果。
通用引用(universal reference)
現在一定有同學感到奇怪了,既然我剛才講的完美轉發就是怎么傳進來怎么傳給別人,那么也就是說在后面這個例子當中我們傳進來的這個參數t
竟然是一個左值!
可是我們的參數表里不是寫着T&&
,要求接受一個右值嗎?
其實不是這樣的。這里就牽扯到一個新的概念,叫做通用引用。
通用引用(universal reference)是Scott Meyers在C++ and Beyond 2012演講中自創的一個詞,用來特指一種引用的類型。構成通用引用有兩個條件:
- 必須滿足
T&&
這種形式 - 類型
T
必須是通過推斷得到的
所以,在我們完美轉發這個部分的例子當中,我們所使用的這種引用,其實是通用引用,而不是所謂的單純的右值引用。因為我們的函數是模板函數,T
的類型是推斷出來的,而不是指定的。那么相應的,如果有一段這樣的代碼:
template <typename T> class TestClass { public: void func(T&& t) {} //這個T&&是不是一個通用引用呢 }
上面的這個T
是不是通用引用呢?答案是不是。因為當這個類初始化的時候這個T
就已經被確定了,不需要推斷。
所以,可以構成通用引用的有如下幾種可能:
-
函數模板參數(function template parameters)
cpp template <typename T> void f(T&& param);
-
auto
聲明(auto declaration)cpp auto && var = ...;
typedef
聲明(typedef declaration)decltype
聲明(decltype declaration)
那么,這個通用引用與其他的引用有什么區別呢?其實最重要的一點就是引用類型合成(Reference Collapsing Rules)。規則很簡單:
T& & => T&
T&& & => T&
T& && => T&
T&& && => T&&
簡單一點說,就是傳進來的如果是左值引用那就是左值引用,如果是右值引用那就是右值引用。但是注意,這個合成規則用戶是不允許使用的,只有編譯器才能夠使用這種合成規則。這就是為什么上面的通用引用當中有一條要求是類型必須可以自動推導。這個合成規則其實就是類型推倒的規則之一。
這樣,我們就可以知道為什么Scott Meyers在演講中建議大家在通用引用的情境下,盡可能使用forward()
了,因為這樣可以在不改變語義的情況下提升性能。
template <typename T> void doWork(T && param) { // ops and expressions using std::forward<T>(param) }
后記
C++0x通過引入許多新的語言特性來實現了語言性能的提升,使得本來就博大精深的一門語言變得更加的難以學習。但是一旦了解,就會被語言精妙的設計所折服。參考資料中給出了更多的關於左值、右值、左值引用、右值引用、移動語義和完美轉發的例子。我自己實在是沒有精力看完所有的這些資料了,各位有興趣的話可以參閱。
參考資料
- http://thbecker.net/articles/rvalue_references/section_01.html#section_01
- http://blog.csdn.net/pongba/article/details/1697636
- http://channel9.msdn.com/Shows/Going+Deep/Cpp-and-Beyond-2012-Scott-Meyers-Universal-References-in-Cpp11
- https://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers
- https://onedrive.live.com/view.aspx?resid=F1B8FF18A2AEC5C5!1062
================================
-
移動構造函數和移動賦值運算符
- 類似拷貝構造函數,移動構造函數的第一個參數是該類類型的一個引用。不同於拷貝構造函數的是,這個引用參數在移動構造函數中是一個右值引用。與拷貝構造函數相同,任何額外的參數都必須有默認實參。
- 除了完成資源移動,移動構造函數還必須確保移后源對象處於這樣一個狀態 — 銷毀它是無害的。特別是,一旦資源完成移動,源對象必須不再指向被移動的資源。這些資源的所有權已經歸屬於新創建的對象。
- 移動操作、標准庫容器和異常
- 由於移動操作“竊取”資源,它通常不會分配任何資源。因此,移動操作通常不會拋出任何異常。
- 當編寫一個不拋出異常的移動操作時,我們應該將此事通知標准庫。除非標准庫知道不會拋出異常,否則它會為了處理可能拋出異常這種可能性而做一些額外的工作。
- 一種通知標准庫的方法是將構造函數指明為 noexcept。這個關鍵字是新標准引入的。
- 不拋出異常的移動構造函數和移動賦值運算符都必須標記為noexcept.
-
移后源對象必須可析構
- 從一個對象移動數據並不會銷毀此對象,但有時在移動操作完成后,源對象會被銷毀。當我們編寫一個移動操作時,必須確保移后源對象進入一個可析構的狀態。即將移后源對象的指針成員置為nullptr來實現的。
- 在移動操作之后,移后源對象必須保持有效的、可析構的狀態,但是用戶不能對其值進行任何假設。
-
合成的移動操作
- 如果一個類定義了自己的拷貝構造函數、拷貝賦值運算符或者析構函數,編譯器就不會為它合成移動構造函數和移動賦值運算符了。
- 只有當一個類沒有定義任何自己版本的拷貝控制成員,且類的每個非static數據都可以移動時,編譯器才會為它合成移動構造函數和移動賦值運算符。
- 定義了一個移動構造函數或移動賦值運算符的類必須也定義自己的拷貝操作。否則,這些成員默認被定義為刪除的。
==========================
https://en.cppreference.com/w/cpp/language/reference#Forwarding_references
Because references are not objects, there are no arrays of references, no pointers to references, and no references to references:
int& a[3]; // error int&* p; // error int& &r; // error
Reference collapsing【template或者typedef才會發生引用折疊】
It is permitted to form references to references through type manipulations in templates or typedefs, in which case the reference collapsing rules apply:
rvalue reference to rvalue reference collapses to rvalue reference, all other combinations form lvalue reference:
typedef int& lref; typedef int&& rref; int n; lref& r1 = n; // type of r1 is int& lref&& r2 = n; // type of r2 is int& rref& r3 = n; // type of r3 is int& rref&& r4 = 1; // type of r4 is int&&
Forwarding references 通用引用
Forwarding references are a special kind of references that preserve the value category of a function argument, making it possible to forward it by means of std::forward. Forwarding references are either:
template<class T> int f(T&& x) { // x is a forwarding reference return g(std::forward<T>(x)); // and so can be forwarded } int main() { int i; f(i); // argument is lvalue, calls f<int&>(int&), std::forward<int&>(x) is lvalue //實參i是個左值,所以模板參數的類型就是int&, T的類型是int&, T&& 是 int& &&, 壓縮為int& f(0); // argument is rvalue, calls f<int>(int&&), std::forward<int>(x) is rvalue //實參0的類型是個右值,所以模板參數的類型是int &&, T的類型就是int } template<class T> int g(const T&& x); // x is not a forwarding reference: const T is not cv-unqualified template<class T> struct A { template<class U> A(T&& x, U&& y, int* p); // x is not a forwarding reference: T is not a // type template parameter of the constructor, // but y is a forwarding reference };
auto&& vec = foo(); // foo() may be lvalue or rvalue, vec is a forwarding reference auto i = std::begin(vec); // works either way (*i)++; // works either way g(std::forward<decltype(vec)>(vec)); // forwards, preserving value category for (auto&& x: f()) { // x is a forwarding reference; this is the safest way to use range for loops } auto&& z = {1, 2, 3}; // *not* a forwarding reference (special case for initializer lists)
See also template argument deduction and std::forward.
#include <iostream> void PrintT(int& t) //左值引用,只接受左值實參; 因為它是非const的 { std::cout << "lvalue" << std::endl; } template <typename T> void PrintT(T&& t) //右值引用,只接受右值實參 { std::cout << "rvalue" << std::endl; } template <typename T> void TestForward(T&& v) //T&&, 自動推導,是通用引用,實參可以是左值,也可以是右值; 但在TestForward函數內部,因為v是個具名的參數,所以內部v又變成了左值。 { PrintT(v); PrintT(std::forward<T>(v)); //完美轉發 PrintT(std::move(v)); //強制變成右值 } void Test() { TestForward(1); int x = 1; TestForward(x); TestForward(std::forward<int>(x)); //函數模板的參數T是int, int && v,所以v是右值
TestForward(std::forward<int&>(x)); //函數模板的參數T是int&, int& &&發生折疊, int &v, 所以v是左值
TestForward(std::forward<int&&>(x)); //函數模板參數是int&&, int&& &&發生折疊,變成int&& v,所以v是右值 } int main(void) { Test(); system("pause"); return 0; }
輸出:
lvalue
rvalue
rvalue
lvalue
lvalue
rvalue
lvalue
rvalue ?why
rvalue
is there a difference between move and forward here:
void test(int && val) { val=4; } void main() { int nb; test(std::forward<int>(nb)); test(std::move(nb)); std::cin.ignore(); }
In your specific case, no, there isn't any difference.
Detailed answer:
Under the hood, std::move(t)
does static_cast<typename std::remove_reference<T>::type&&>(t)
, where T
is type of t
. In your case, it resolves to static_cast<int&&>(nb)
. //強制轉換成type&&右值引用,不存在T&&的引用折疊
forward
is a little bit tricky, because it is tailored for use in templates (to allow perfect forwarding) and not as a tool to cast lvalue to rvalue reference. //forward是為了完美轉發,而不是為了將左值轉換成右值,雖然它也能進行左值到右值的轉換
Standard library provides two overloads (one for lvalue references and the second for rvalue ones):
template <class T> T&& forward(typename remove_reference<T>::type& t) noexcept; //接受左值引用,返回通用引用, 如果T是int&,則返回值展開后是int& &&,會被折疊成int&, 是左值; 如果T是int,則返回值展開后是int&&, 無需折疊,是右值;如果T是int&&, 展開后是int&& &&,折疊為int&&,是右值。 template <class T> T&& forward(typename remove_reference<T>::type&& t) noexcept; //接受右值引用,返回通用引用,折疊同上。 唯一的區別是實參的類型,用來重載匹配。
Substituting int
, we get:
int&& forward(int& t) noexcept; int&& forward(int&& t) noexcept;
And since nb
is lvalue, the first version is chosen.
According to standard draft, the only effect of forward
is static_cast<T&&>(t)
. With T
being int
, we get static_cast<int&&>(nb)
, i.e. - we get two exactly same casts.
Now, if you want to cast lvalue to rvalue (to allow moving), please use only std::move
, which is the idiomatic way to do this conversion. std::forward
is not intended to be used this way.