C++11右值引用和移動語義的實例解析_第1頁
C++11右值引用和移動語義的實例解析_第2頁
C++11右值引用和移動語義的實例解析_第3頁
C++11右值引用和移動語義的實例解析_第4頁
C++11右值引用和移動語義的實例解析_第5頁
已閱讀5頁,還剩21頁未讀 繼續免費閱讀

下載本文檔

版權說明:本文檔由用戶提供并上傳,收益歸屬內容提供方,若內容存在侵權,請進行舉報或認領

文檔簡介

第C++11右值引用和移動語義的實例解析目錄基本概念左值vs右值左值引用vs右值引用右值引用使用場景和意義左值引用的使用場景左值引用的短板右值引用和移動語義右值引用引用左值右值引用的其他使用場景完美轉發萬能引用完美轉發保持值的屬性完美轉發的使用場景總結

基本概念

左值vs右值

什么是左值?

左值是一個表示數據的表達式,如變量名或解引用的指針。

左值可以被取地址,也可以被修改(const修飾的左值除外)。左值可以出現在賦值符號的左邊,也可以出現在賦值符號的右邊。

intmain()

//以下的p、b、c、*p都是左值

int*p=newint(0);

intb=1;

constintc=2;

return0;

什么是右值?

右值也是一個表示數據的表達式,如字母常量、表達式的返回值、函數的返回值(不能是左值引用返回)等等。

右值不能被取地址,也不能被修改。右值可以出現在賦值符號的右邊,但是不能出現在賦值符號的左邊。

intmain()

doublex=1.1,y=2.2;

//以下幾個都是常見的右值

x+y;

fmin(x,y);

//錯誤示例(右值不能出現在賦值符號的左邊)

//10=1;

//x+y=1;

//fmin(x,y)=1;

return0;

右值本質就是一個臨時變量或常量值,比如代碼中的10就是常量值,表達式x+y和函數fmin的返回值就是臨時變量,這些都叫做右值。這些臨時變量和常量值并沒有被實際存儲起來,這也就是為什么右值不能被取地址的原因,因為只有被存儲起來后才有地址。但需要注意的是,這里說函數的返回值是右值,指的是傳值返回的函數,因為傳值返回的函數在返回對象時返回的是對象的拷貝,這個拷貝出來的對象就是一個臨時變量。

而對于左值引用返回的函數來說,這些函數返回的是左值。比如string類實現的[]運算符重載函數:

namespacecl

//模擬實現string類

classstring

public:

//[]運算符重載(可讀可寫)

charoperator[](size_ti)

assert(i_size);//檢測下標的合法性

return_str[i];//返回對應字符

//...

private:

char*_str;//存儲字符串

size_t_size;//記錄字符串當前的有效長度

//...

intmain()

cl::strings("hello");

s[3]='x';//引用返回,支持外部修改

return0;

這里的[]運算符重載函數返回的是一個字符的引用,因為它需要支持外部對該位置的字符進行修改,所以必須采用左值引用返回。之所以說這里返回的是一個左值,是因為這個返回的字符是被存儲起來了的,是存儲在string對象的_str對象當中的,因此這個字符是可以被取到地址的。

左值引用vs右值引用

傳統的C++語法中就有引用的語法,而C++11中新增了右值引用的語法特性,為了進行區分,于是將C++11之前的引用就叫做左值引用。但是無論左值引用還是右值引用,本質都是給對象取別名。

左值引用

左值引用就是對左值的引用,給左值取別名,通過來聲明。比如:

intmain()

//以下的p、b、c、*p都是左值

int*p=newint(0);

intb=1;

constintc=2;

//以下幾個是對上面左值的左值引用

int*rp=p;

intrb=b;

constintrc=c;

intpvalue=*p;

return0;

右值引用

右值引用就是對右值的引用,給右值取別名,通過來聲明。比如:

intmain()

doublex=1.1,y=2.2;

//以下幾個都是常見的右值

x+y;

fmin(x,y);

//以下幾個都是對右值的右值引用

intrr1=10;

doublerr2=x+y;

doublerr3=fmin(x,y);

return0;

需要注意的是,右值是不能取地址的,但是給右值取別名后,會導致右值被存儲到特定位置,這時這個右值可以被取到地址,并且可以被修改,如果不想讓被引用的右值被修改,可以用const修飾右值引用。比如:

intmain()

doublex=1.1,y=2.2;

intrr1=10;

constdoublerr2=x+y;

rr1=20;

rr2=5.5;//報錯

return0;

左值引用可以引用右值嗎?

左值引用不能引用右值,因為這涉及權限放大的問題,右值是不能被修改的,而左值引用是可以修改。但是const左值引用可以引用右值,因為const左值引用能夠保證被引用的數據不會被修改。

因此const左值引用既可以引用左值,也可以引用右值。比如:

templateclassT

voidfunc(constTval)

coutvalendl;

intmain()

strings("hello");

func(s);//s為左值

func("world");//"world"為右值

return0;

右值引用可以引用左值嗎?

右值引用只能引用右值,不能引用左值。但是右值引用可以引用move以后的左值。

move函數是C++11標準提供的一個函數,被move后的左值能夠賦值給右值引用。比如:

intmain()

inta=10;

//intr1=a;//右值引用不能引用左值

intr2=move(a);//右值引用可以引用move以后的左值

return0;

右值引用使用場景和意義

雖然const左值引用既能接收左值,又能接收右值,但左值引用終究存在短板,而C++11提出的右值引用就是用來解決左值引用的短板的。

為了更好的說明問題,這里需要借助一個深拷貝的類,下面模擬實現了一個簡化版的string類。類當中實現了一些基本的成員函數,并在string的拷貝構造函數和賦值運算符重載函數當中打印了一條提示語句,這樣當調用這兩個函數時我們就能夠知道。

代碼如下:

namespacecl

classstring

public:

typedefchar*iterator;

iteratorbegin()

return_str;//返回字符串中第一個字符的地址

iteratorend()

return_str+_size;//返回字符串中最后一個字符的后一個字符的地址

//構造函數

string(constchar*str="")

_size=strlen(str);//初始時,字符串大小設置為字符串長度

_capacity=_size;//初始時,字符串容量設置為字符串長度

_str=newchar[_capacity+1];//為存儲字符串開辟空間(多開一個用于存放'\0')

strcpy(_str,str);//將C字符串拷貝到已開好的空間

//交換兩個對象的數據

voidswap(strings)

//調用庫里的swap

::swap(_str,s._str);//交換兩個對象的C字符串

::swap(_size,s._size);//交換兩個對象的大小

::swap(_capacity,s._capacity);//交換兩個對象的容量

//拷貝構造函數(現代寫法)

string(conststrings)

:_str(nullptr)

,_size(0)

,_capacity(0)

cout"string(conststrings)--深拷貝"endl;

stringtmp(s._str);//調用構造函數,構造出一個C字符串為s._str的對象

swap(tmp);//交換這兩個對象

//賦值運算符重載(現代寫法)

stringoperator=(conststrings)

cout"stringoperator=(conststrings)--深拷貝"endl;

stringtmp(s);//用s拷貝構造出對象tmp

swap(tmp);//交換這兩個對象

return*this;//返回左值(支持連續賦值)

//析構函數

~string()

delete[]_str;//釋放_str指向的空間

_str=nullptr;//及時置空,防止非法訪問

_size=0;//大小置0

_capacity=0;//容量置0

//[]運算符重載

charoperator[](size_ti)

assert(i_size);//檢測下標的合法性

return_str[i];//返回對應字符

//改變容量,大小不變

voidreserve(size_tn)

if(n_capacity)//當n大于對象當前容量時才需執行操作

char*tmp=newchar[n+1];//多開一個空間用于存放'\0'

strncpy(tmp,_str,_size+1);//將對象原本的C字符串拷貝過來(包括'\0')

delete[]_str;//釋放對象原本的空間

_str=tmp;//將新開辟的空間交給_str

_capacity=n;//容量跟著改變

//尾插字符

voidpush_back(charch)

if(_size==_capacity)//判斷是否需要增容

reserve(_capacity==04:_capacity*2);//將容量擴大為原來的兩倍

_str[_size]=ch;//將字符尾插到字符串

_str[_size+1]='\0';//字符串后面放上'\0'

_size++;//字符串的大小加一

//+=運算符重載

stringoperator+=(charch)

push_back(ch);//尾插字符串

return*this;//返回左值(支持連續+=)

//返回C類型的字符串

constchar*c_str()const

return_str;

private:

char*_str;

size_t_size;

size_t_capacity;

左值引用的使用場景

在說明左值引用的短板之前,我們先來看看左值引用的使用場景:

左值引用做參數,防止傳參時進行拷貝操作。左值引用做返回值,防止返回時對返回對象進行拷貝操作。

voidfunc1(cl::strings)

voidfunc2(constcl::strings)

intmain()

cl::strings("helloworld");

func1(s);//值傳參

func2(s);//左值引用傳參

s+='X';//左值引用返回

return0;

因為我們模擬實現是string類的拷貝構造函數當中打印了提示語句,因此運行代碼后通過程序運行結果就知道,值傳參時調用了string的拷貝構造函數。

此外,因為string的+=運算符重載函數是左值引用返回的,因此在返回+=后的對象時不會調用拷貝構造函數,但如果將+=運算符重載函數改為傳值返回,那么重新運行代碼后你就會發現多了一次拷貝構造函數的調用。

我們都知道string的拷貝是深拷貝,深拷貝的代價是比較高的,我們應該盡量避免不必要的深拷貝操作,因此這里左值引用起到的作用還是很明顯的。

左值引用的短板

左值引用雖然能避免不必要的拷貝操作,但左值引用并不能完全避免。

左值引用做參數,能夠完全避免傳參時不必要的拷貝操作。左值引用做返回值,并不能完全避免函數返回對象時不必要的拷貝操作。

如果函數返回的對象是一個局部變量,該變量出了函數作用域就被銷毀了,這種情況下不能用左值引用作為返回值,只能以傳值的方式返回,這就是左值引用的短板。

比如下面我們模擬實現一個int版本的to_string函數,這個to_string函數就不能使用左值引用返回,因為to_string函數返回的是一個局部變量。

代碼如下:

namespacecl

cl::stringto_string(intvalue)

boolflag=true;

if(value0)

flag=false;

value=0-value;

cl::stringstr;

while(value0)

intx=value%10;

value/=10;

str+=(x+'0');

if(flag==false)

str+='-';

std::reverse(str.begin(),str.end());

returnstr;

此時調用to_string函數返回時,就一定會調用string的拷貝構造函數。比如:

intmain()

cl::strings=cl::to_string(1234);

return0;

C++11提出右值引用就是為了解決左值引用的這個短板的,但解決方式并不是簡單的將右值引用作為函數的返回值。

右值引用和移動語義

右值引用和移動語句解決上述問題的方式就是,給當前模擬實現的string類增加移動構造和移動賦值方法。

移動構造

移動構造是一個構造函數,該構造函數的參數是右值引用類型的,移動構造本質就是將傳入右值的資源竊取過來,占為己有,這樣就避免了進行深拷貝,所以它叫做移動構造,就是竊取別人的資源來構造自己的意思。

在當前的string類中增加一個移動構造函數,該函數要做的就是調用swap函數將傳入右值的資源竊取過來,為了能夠更好的得知移動構造函數是否被調用,可以在該函數當中打印一條提示語句。

代碼如下:

namespacecl

classstring

public:

//移動構造

string(strings)

:_str(nullptr)

,_size(0)

,_capacity(0)

cout"string(strings)--移動構造"endl;

swap(s);

private:

char*_str;

size_t_size;

size_t_capacity;

移動構造和拷貝構造的區別:

在沒有增加移動構造之前,由于拷貝構造采用的是const左值引用接收參數,因此無論拷貝構造對象時傳入的是左值還是右值,都會調用拷貝構造函數。增加移動構造之后,由于移動構造采用的是右值引用接收參數,因此如果拷貝構造對象時傳入的是右值,那么就會調用移動構造函數(最匹配原則)。string的拷貝構造函數做的是深拷貝,而移動構造函數中只需要調用swap函數進行資源的轉移,因此調用移動構造的代價比調用拷貝構造的代價小。

給string類增加移動構造后,對于返回局部string對象的這類函數,在返回string對象時就會調用移動構造進行資源的移動,而不會再調用拷貝構造函數進行深拷貝了。比如:

intmain()

cl::strings=cl::to_string(1234);

return0;

說明一下:

雖然to_string當中返回的局部string對象是一個左值,但由于該string對象在當前函數調用結束后就會立即被銷毀,我可以把這種即將被消耗的值叫做將亡值,比如匿名對象也可以叫做將亡值。既然將亡值馬上就要被銷毀了,那還不如把它的資源轉移給別人用,因此編譯器在識別這種將亡值時會將其識別為右值,這樣就可以匹配到參數類型為右值引用的移動構造函數。

編譯器做的優化

實際當一個函數在返回局部對象時,會先用這個局部對象拷貝構造出一個臨時對象,然后再用這個臨時對象來拷貝構造我們接收返回值的對象。如下:

因此在C++11標準出來之前,對于深拷貝的類來說這里就會進行兩次深拷貝,所以大部分編譯器為了提高效率都對這種情況進行了優化,這種連續調用構造函數的場景通常會被優化成一次。比如:

因此按道理來說,在C++11標準出來之前這里應該調用兩次string的拷貝構造函數,但最終被編譯器優化成了一次,減少了一次無意義的深拷貝。(并不是所有的編譯器都做了這個優化)

在C++11出來之后,編譯器的這個優化仍然起到了作用。

如果編譯器不優化這里應該調用兩次移動構造,第一次調用移動構造用返回的局部string對象構造出一個臨時對象,第二次調用移動構造用這個臨時對象構造接收返回值的對象。而經過編譯器優化后,最終這兩次移動構造就被優化成了一次,也就是直接將返回的局部string對象的資源移動給了接收返回值的對象。此外,C++11之后就算編譯器沒有進行這個優化問題也不大,因為不優化也就是調用兩次移動構造進行兩次資源的轉移而已。

但如果我們不是用函數的返回值來構造一個對象,而是用一個之前已經定義出來的對象來接收函數的返回值,這時編譯器就無法進行優化了。比如:

這時當函數返回局部對象時,會先用這個局部對象拷貝構造出一個臨時對象,然后再調用賦值運算符重載函數將這個臨時對象賦值給接收函數返回值的對象。

編譯器并沒有對這種情況進行優化,因此在C++11標準出來之前,對于深拷貝的類來說這里就會存在兩次深拷貝,因為深拷貝的類的賦值運算符重載函數也需要以深拷貝的方式實現。但在深拷貝的類中引入C++11的移動構造后,這里仍然需要再調用一次賦值運算符重載函數進行深拷貝,因此深拷貝的類不僅需要實現移動構造,還需要實現移動賦值。

這里需要說明的是,對于返回局部對象的函數,就算只是調用函數而不接收該函數的返回值,也會存在一次拷貝構造或移動構造,因為函數的返回值不管你接不接收都必須要有,而當函數結束后該函數內的局部對象都會被銷毀,所以就算不接收函數的返回值也會調用一次拷貝構造或移動構造生成臨時對象。

移動賦值

移動賦值是一個賦值運算符重載函數,該函數的參數是右值引用類型的,移動賦值也是將傳入右值的資源竊取過來,占為己有,這樣就避免了深拷貝,所以它叫移動賦值,就是竊取別人的資源來賦值給自己的意思。

在當前的string類中增加一個移動賦值函數,該函數要做的就是調用swap函數將傳入右值的資源竊取過來,為了能夠更好的得知移動賦值函數是否被調用,可以在該函數中打印一條提示語句。

代碼如下:

namespacecl

classstring

public:

//移動賦值

stringoperator=(strings)

cout"stringoperator=(strings)--移動賦值"endl;

swap(s);

return*this;

private:

char*_str;

size_t_size;

size_t_capacity;

移動賦值和原有operator=函數的區別:

在沒有增加移動賦值之前,由于原有operator=函數采用的是const左值引用接收參數,因此無論賦值時傳入的是左值還是右值,都會調用原有的operator=函數。增加移動賦值之后,由于移動賦值采用的是右值引用接收參數,因此如果賦值時傳入的是右值,那么就會調用移動賦值函數(最匹配原則)。string原有的operator=函數做的是深拷貝,而移動賦值函數中只需要調用swap函數進行資源的轉移,因此調用移動賦值的代價比調用原有operator=的代價小。

現在給string增加移動構造和移動賦值以后,就算是用一個已經定義過的string對象去接收to_string函數的返回值,此時也不會存在深拷貝。比如:

intmain()

cl::strings;

//...

s=cl::to_string(1234);

return0;

此時當to_string函數返回局部的string對象時,會先調用移動構造生成一個臨時對象,然后再調用移動賦值將臨時對象的資源轉移給我們接收返回值的對象,這個過程雖然調用了兩個函數,但這兩個函數要做的只是資源的移動,而不需要進行深拷貝,大大提高了效率。

說明一下:在實現移動賦值函數之前,該代碼的運行結果理論上應該是調用一次拷貝構造,再調用一次原有的operator=函數,但由于原有operator=函數實現時復用了拷貝構造函數,因此代碼運行后的輸出結果會多打印一次拷貝構造函數的調用,這是原有operator=函數內部調用的。

STL中的容器

C++11標準出來之后,STL中的容器都增加了移動構造和移動賦值。

以我們剛剛說的string類為例,這是string類增加的移動構造:

這是string類增加的移動賦值:

右值引用引用左值

右值引用雖然不能引用左值,但也不是完全不可以,當需要用右值引用引用一個左值時,可以通過move函數將左值轉化為右值。

move函數的名字具有迷惑性,move函數實際并不能搬移任何東西,該函數唯一的功能就是將一個左值強制轉化為右值引用,然后實現移動語義。

move函數的定義如下:

templateclass_Ty

inlinetypenameremove_reference_Ty::typemove(_Ty_Arg)_NOEXCEPT

//forward_Argasmovable

return((typenameremove_reference_Ty::type)_Arg);

說明一下:

move函數中_Arg參數的類型不是右值引用,而是萬能引用。萬能引用跟右值引用的形式一樣,但是右值引用需要是確定的類型。一個左值被move以后,它的資源可能就被轉移給別人了,因此要慎用一個被move后的左值。

右值引用的其他使用場景

右值引用版本的插入函數

C++11標準出來之后,STL中的容器除了增加移動構造和移動賦值之外,STL容器插入接口函數也增加了右值引用版本。

以list容器的push_back接口為例:

右值引用版本插入函數的意義

如果list容器當中存儲的是string對象,那么在調用push_back向list容器中插入元素時,可能會有如下幾種插入方式:

intmain()

listcl::string

cl::strings("1111");

lt.push_back(s);//調用string的拷貝構造

lt.push_back("2222");//調用string的移動構造

lt.push_back(cl::string("3333"));//調用string的移動構造

lt.push_back(std::move(s));//調用string的移動構造

return0;

list容器的push_back函數需要先構造一個結點,然后將該結點插入到底層的雙鏈表當中。

在C++11之前list容器的push_back接口只有一個左值引用版本,因此在push_back函數中構造結點時,這個左值只能匹配到string的拷貝構造函數進行深拷貝。而在C++11出來之后,string類提供了移動構造函數,并且list容器的push_back接口提供了右值引用版本,此時如果傳入push_back函數的string對象是一個右值,那么在push_back函數中構造結點時,這個右值就可以匹配到string的移動構造函數進行資源的轉移,這樣就避免了深拷貝,提高了效率。上述代碼中的插入第一個元素時就會匹配到push_back的左值引用版本,在push_back函數內部就會調用string的拷貝構造函數進行深拷貝,而插入后面三個元素時由于傳入的是右值,因此會匹配到push_back的右值引用版本,此時在push_back函數內部就會調用string的移動構造函數進行資源的轉移。

完美轉發

萬能引用

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

templateclassT

voidPerfectForward(Tt)

//...

右值引用和萬能引用的區別就是,右值引用需要是確定的類型,而萬能引用是根據傳入實參的類型進行推導,如果傳入的實參是一個左值,那么這里的形參t就是左值引用,如果傳入的實參是一個右值,那么這里的形參t就是右值引用。

下面重載了四個Func函數,這四個Func函數的參數類型分別是左值引用、const左值引用、右值引用和const右值引用。在主函數中調用PerfectForward函數時分別傳入左值、右值、const左值和const右值,在PerfectForward函數中再調用Func函數。如下:

voidFunc(intx)

cout"左值引用"endl;

voidFunc(constintx)

cout"const左值引用"endl;

voidFunc(intx)

cout"右值引用"endl;

voidFunc(constintx)

cout"const右值引用"endl;

templateclassT

voidPerfectForward(Tt)

Func(t);

intmain()

inta=10;

PerfectForward(a);//左值

PerfectForward(move(a));//右值

constintb=20;

PerfectForward(b);//const左值

PerfectForward(move(b));//const右值

return0;

由于PerfectForward函數的參數類型是萬能引用,因此既可以接收左值也可以接收右值,而我們在PerfectForward函數中調用Func函數,就是希望調用PerfectForward函數時傳入左值、右值、const左值、const右值,能夠匹配到對應版本的Func函數。

但實際調用PerfectForward函數時傳入左值和右值,最終都匹配到了左值引用版本的Func函數,調用PerfectForward函數時傳入const左值和const右值,最終都匹配到了const左值引用版本的Func函數。根本原因就是,右值被引用后會導致右值被存儲到特定位置,這時這個右值可以被取到地址,并且可以被修改,所以在PerfectForward函數中調用Func函數時會將t識別成左值。

也就是說,右值經過一次參數傳遞后其屬性會退化成左值,如果想要在這個過程中保持右值的屬性,就需要用到完美轉發。

完美轉發保持值的屬性

要想在參數傳遞過程中保持其原有的屬性,需要在傳參時調用forward函數。比如:

templateclassT

voidPerfectForward(Tt)

Func(std::forwardT(t));

經過完美轉發后,調用PerfectForward函數時傳入的是右值就會匹配到右值引用版本的Func函數,傳入的是const右值就會匹配到const右值引用版本的Func函數,這就是完美轉發的價值。

完美轉發的使用場景

下面模擬實現了一個簡化版的list類,類當中分別提供了左值引用版本和右值引用版本的push_back和insert函數。

代碼如下:

namespacecl

templateclassT

structListNode

T_data;

ListNode*_next=nullptr;

ListNode*_prev=nullptr;

templateclassT

classlist

typedefListNodeTnode;

public:

//構造函數

list()

_head=newnode;

_head-_next=_head;

_head-_prev=_head;

//左值引用版本的push_back

voidpush_back(constTx)

insert(_head,x);

//右值引用版本的push_back

voidpush_back(Tx)

insert(_head,std::forwardT(x));//完美轉發

//左值引用版本的insert

voidinsert(node*pos,constTx)

node*prev=pos-_prev;

node*newnode=newnode;

newnode-_data=x;

prev-_next=newnode;

newnode-_prev=prev;

newnode-_next=pos;

pos-_prev=newnode;

//右值引用版本的insert

voidinsert(node*pos,Tx)

node*prev=pos-_prev;

node*newnode=newnode;

newnode-_data=std::forwardT//完美轉發

prev-_next=newnode;

newnode-_prev=prev;

newnode-_next=pos;

pos-_prev=newnode;

private:

node*_head;//指向鏈表頭結點的指針

下面定義一個list對象,list容器中存儲的就是之前模擬實現的string類,這里分別傳入左值和右值調用不同版本的push_back。比如:

intmain()

cl::listcl::string

cl::strings("1111");

lt.push_back(s);//調用左值引用版

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯系上傳者。文件的所有權益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網頁內容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
  • 4. 未經權益所有人同意不得將文件中的內容挪作商業或盈利用途。
  • 5. 人人文庫網僅提供信息存儲空間,僅對用戶上傳內容的表現方式做保護處理,對用戶上傳分享的文檔內容本身不做任何修改或編輯,并不能對任何下載內容負責。
  • 6. 下載文件中如有侵權或不適當內容,請與我們聯系,我們立即糾正。
  • 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論