[C/C++]關于C++11中的std::move和std::forward

http://blog.sina.com.cn/s/blog_53b7ddf00101p5t0.html

std::move是一個用于提示優化的函數,過去的c++98中,由于無法將作為右值的臨時變量從左值當中區別出來,所以程序運行時有大量臨時變量白白的創建后又立刻銷毀,其中又尤其是返回字符串std::string的函數存在最大的浪費。

比如:

1 std::string fileContent = “oldContent”;
2 s = readFileContent(fileName);

因為并不是所有情況下,C++編譯器都能進行返回值優化,所以,向上面的例子中,往往會創建多個字符串。readFileContent如果沒有內部狀態,那么,它的返回值多半是std::string(const std::string的做法不再被推薦了),而不是const std::string&。這是一個浪費,函數的返回值被拷貝到s中后,棧上的臨時對象就被銷毀了。

在C++11中,編碼者可以主動提示編譯器,readFileContent返回的對象是臨時的,可以被挪作他用:std::move。

將上面的例子改成:

1 std::string fileContent = “oldContent”;
2 s = std::move(readFileContent(fileName));

后,對象s在被賦值的時候,方法std::string::operator =(std::string&&)會被調用,符號&&告訴std::string類的編寫者,傳入的參數是一個臨時對象,可以挪用其數據,于是std::string::operator =(std::string&&)的實現代碼中,會置空形參,同時將原本保存在中形參中的數據移動到自身。

不光是臨時變量,只要是你認為不再需要的數據,都可以考慮用std::move移動。

比較有名的std::move用法是在swap中:

復制代碼
1 template
2 void swap(T& a, T& b)
3 {
4 T t(std::move(a)); // a為空,t占有a的初始數據
5 a = std::move(b); // b為空, a占有b的初始數據
6 b = std::move(t); // t為空,b占有a的初始數據
7 }
復制代碼

總之,std::move是為性能而生的,正式因為了有了這個主動報告廢棄物的設施,所以C++11中的STL性能大幅提升,即使C++用戶仍然按找舊有的方式來編碼,仍然能因中新版STL等標準庫的強化中收益。

?

std::forward是用于模板編程中的,如果不需要編寫通用的模板類和函數,可能不怎么用的上它。

要認識它的作用,需要知道C++中的幾條規則:(這里有篇挺好的文章:http://blog.csdn.net/zwvista/article/details/6848582,但似乎因標準的更新,其中的規則已不完全成立了)

1.?引用折疊規則:

X& + & => X&
X&& + & => X&
X& + && => X&
X&& + && => X&&

2. 對于模板函數中的形參聲明T&&(這里的模板參數T,最終推演的結果可能不是一個純類型,它可能還會帶有引用/常量修飾符,如,T推演為const int時,實際形參為const int &&),會有如下規則:

如果調用函數時的實參為U&(這里的U可能有const/volatile修飾,但沒有左/右引用修飾了),那么T推演為U&,顯然根據上面的引用折疊規則,U& &&=>U&。

如果調用實參為U&&,雖然將T推導為U&&和U都能滿足折疊規則(U&& &&=> U&&且U &&=>U&&),但標準規定,這里選擇將T推演為U而非U&&。

總結一下第2條規則:當形參聲明為T&&時,對于實參U&,T被推演為U&;當實參是U&&時,T被推演為U。當然,T和U具有相同的const/volatile屬性。

3.這點很重要,也是上面zwvista的文章中沒有提到的:形參T&& t中的變量t,始終是左值引用,即使調用函數的實參是右值引用也不例外。可以這么理解,本來,左值和右值概念的本質區別就是,左值是用戶顯示聲明或分配內存的變量,能夠直接用變量名訪問,而右值主要是臨時變量。當一個臨時變量傳入形參為T&& t的模板函數時,T被推演為U,參數t所引用的臨時變量因為開始能夠被據名訪問了,所以它變成了左值。這也就是std::forward存在的原因!當你以為實參是右值所以t也應該是右值時,它跟你開了個玩笑,它是左值!如果你要進一步調用的函數會根據左右值引用性來進行不同操作,那么你在將t傳給其他函數時,應該先用std::forward恢復t的本來引用性,恢復的依據是模板參數T的推演結果。雖然t的右值引用行會退化,變成左值引用,但根據實參的左右引用性不同,T會被分別推演為U&和U,這就是依據!因此傳給std::forward的兩個參數一個都不能少:std::forward(t)。

?

再來,討論一下,一個模板函數如果要保留參數的左右值引用性,為什么應該聲明為T&&:

如果聲明函數f(T t):實參會直接進行值傳遞,失去了引用性。

如果聲明函數f(T &t): 根據引用折疊法則,無論T是U&還是U&&,T&的折疊結果都只會是U&,即,這個聲明不能用于匹配右值引用實參。

如果聲明函數f(T &&t): 如果T為U&,T&&的結果是U&,可以匹配左值實參;如果T為U&&,T&&的結果是U&&,可以匹配右值實參。又因為T的cv性和U相同,所以這種聲明能夠保留實參的類型信息。

?

先來看一組幫助類:

1 template struct TypeName { static const char *get(){ return "Type"; } };
2 template struct TypeName<<SPAN style="PADDING-BOTTOM: 0px; LINE-HEIGHT: 1.5; MARGIN: 0px; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New'; COLOR: rgb(0,0,255); FONT-SIZE: 12px; PADDING-TOP: 0px">const T> { static const char *get(){ return "const Type"; } };
3 template struct TypeName { static const char *get(){ return "Type&"; } };
4 template struct TypeName<<SPAN style="PADDING-BOTTOM: 0px; LINE-HEIGHT: 1.5; MARGIN: 0px; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New'; COLOR: rgb(0,0,255); FONT-SIZE: 12px; PADDING-TOP: 0px">const T&> { static const char *get(){ return "const Type&"; } };
5 template struct TypeName { static const char *get(){ return "Type&&"; } };
6 template struct TypeName<<SPAN style="PADDING-BOTTOM: 0px; LINE-HEIGHT: 1.5; MARGIN: 0px; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New'; COLOR: rgb(0,0,255); FONT-SIZE: 12px; PADDING-TOP: 0px">const T&&> { static const char *get(){ return "const Type&&"; } };

在模板函數內部將模板參數T傳給TypeName,就可以訪問T的類型字符串:TypeName::get()。

?

再一個幫助函數,用于打印一個表達式的類型:

1 template
2 void printValType(T &&val)
3 {
4 cout << TypeName::get() << endl;
5 }

注意3條規則在這個模板函數上的應用。規則1,解釋了T&& val的聲明足以保留實參的類型信息。規則2,說明了,當實參是string&時,T就是string&;當實參是const string&&時,T就是const string(而非const string&&)。規則3,強調,無論實參是string&還是string&&,形參val的類型都是string&!

注意TypeName的寫法,因為T只能為U&或者U,顯然T&&可以根據折疊法則還原為實參類型U&和U&&。

?

這里是常見的const/左右引用組合的情形:

1 class A{}; // 測試類
2 A& lRefA() { static A a; return a;} // 左值
3 const A& clRefA() { static A a; return a;} // 常左值
4 A rRefA() { return A(); } // 右值
5 const A crRefA() { return A(); } // 常右值

測試一下上面的表達式類型:

1 printValType(lRefA());
2 printValType(clRefA());
3 printValType(rRefA());
4 printValType(crRefA());

輸出依次是: Type&,const Type&,Type&&,const Type&&。

?

現在正式來探討std::forward的實現。

回顧一下使用std::forward的原因:由于聲明為f(T&& t)的模板函數的形參t會失去右值引用性質,所以在將t傳給更深層函數前,可能會需要回復t的正確引用行,當然,修改t的引用性辦不到,但根據t返回另一個引用還是可以的。恰好,上面的函數printValType是一個會根據實參類型不同,作出不同反映的函數,所以可以把它作為f的內層函數,來檢測f有沒有正確的修正t的引用行。

復制代碼
 1 template
2 void f(T &&a)
3 {
4 printValType(a);
5 }
6
7 int main()
8 {
9 f(lRefA());
10 f(clRefA());
11 f(rRefA());
12 f(crRefA());
13 }
復制代碼

輸出:Type&,const Type&,Type&,const Type&。

可見后兩個輸出錯了,這正是前面規則3描述的,當實參是右值引用時,雖然T被推演為U,但是參數a退化成了左值引用。

直接應用std::forward:

1 template
2 void f(T &&a)
3 {
4 printValType(std::forward(a));
5 }

輸出:Type&,const Type&,Type&&,const Type&&。

輸出正確了,這就是std::forward的作用啊。如果更深層的函數也需要完整的引用信息,如這里的printValType,那就應該在傳遞形參前先std::forward!

?

在編寫自己的forward函數之前,先來嘗試直接強制轉化參數a:

1 template
2 void f(T &&a)
3 {
4 printValType((T&&)a);
5 }

輸出:Type&,const Type&,Type&&,const Type&&。

正確!因為不管T被推演為U&還是U,只要T&&肯定能還原為U&和U&&。

?

考慮下自己的forward函數應該怎么寫:

?因為在forward的調用方中,形參已經丟失了右值引用信息,唯一的參考依據是T,要根據T還原為正確的參數,得T&&,因此,強制轉換和返回類型都是T&&了,當然,forward還必須被以forward()的方式顯示指定模板類型,這樣才能保證forward的模板參數T和上層函數f的T是相同類型。首先:

1 template
2 T&& forward(... a)
3 {
4 return (T&&)a;
5 }

調用方f一定得顯示指定類型forward。

形參怎么寫?形參a的類型由T構成,而且forward的實參一定是左值(暫時不考慮forward(std::string())的使用方法),也就是說,無論T是U&還是U,形參a的類型一定都得是U&,才能和實參匹配,所以,結果是:

1 template
2 T&& forward(T& a)
3 {
4 return (T&&)a;
5 }

測試,輸出:Type&,const Type&,Type&&,const Type&&。
正確!

?

再試下,如果f調用forward的時候,使用forward(a)的方式,沒有顯示指定模板類型會怎么樣:

1 template
2 void f(T &&a)
3 {
4 printValType(forward(a));
5 }

輸出:T&&,const Type&&,Type&&,const Type&&。

錯了。分析下,因為實參始終是左值,所以forward的形參T& a中,T就被推演為U,因此(T&&)a也就是(U&&)a所以結果錯誤。

為了避免用戶使用forward(a),因此應該禁用forward的自動模板參數推演功能!可以借助std::identity,另外,將(T&&)換成static_cast,規范一下:

1 template
2 T&& forward(typename std::identity::type& a)
3 {
4 return static_cast(a);
5 }

?

上面講的是針對T為U&或U,而實參始終為左值的情況,這是常見的情形;不過也有實參為右值的情況,還需要改進上面這個forward,但我這里就不寫了。

這是我手里的gcc4.5.2的forward實現:

復制代碼
 1   /// forward (as per N2835)
2 /// Forward lvalues as rvalues.
3 template
4 inline typename enable_if::value, _Tp&&>::type
5 forward(typename std::identity<_Tp>::type& __t)
6 { return static_cast<_Tp&&>(__t); }
7
8 /// Forward rvalues as rvalues.
9 template
10 inline typename enable_if::value, _Tp&&>::type
11 forward(typename std::identity<_Tp>::type&& __t)
12 { return static_cast<_Tp&&>(__t); }
13
14 // Forward lvalues as lvalues.
15 template
16 inline typename enable_if::value, _Tp>::type
17 forward(typename std::identity<_Tp>::type __t)
18 { return __t; }
19
20 // Prevent forwarding rvalues as const lvalues.
21 template
22 inline typename enable_if::value, _Tp>::type
23 forward(typename std::remove_reference<_Tp>::type&& __t) = delete;
復制代碼

第1/3版本就相當于我之前的實現,而版本2/4是實參為右值的情況,至于后者這種取舍的原因,還得去自己研究下使用場合和文檔了。

我手里的vc2010實現的forward和我之前的實現相同,顯然還不夠,不過vc2010本來對標準也就還支持得少..


本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/383860.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/383860.shtml
英文地址,請注明出處:http://en.pswp.cn/news/383860.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

BZOJ3930-莫比烏斯反演+杜教篩

題目的意思很簡單&#xff0c;求給定區間內的gcdk的個數&#xff0c;這應該是傳統的莫比烏斯反演了。 有兩種思路&#xff0c;一種是直接將里面變成gcd1&#xff0c;然后里面看作元函數用莫比烏斯函數和恒等函數展開&#xff0c;然后改變求和順序。 還有一種是構造兩個函數&…

HDU1999不可摸數-暴力打表

看到這約數和第一反應是約數和函數&#xff0c;然后仔細一看不是正經的約數和函數&#xff0c;就去推去了&#xff0c;然后推的有點小復雜。&#xff08;數論函數那部分做多了&#xff09; 然后觀察也沒有用到什么數論部分的特殊知識啊&#xff0c;難不成真的要暴力&#xff1f…

BZOJ2818-莫比烏斯反演/歐拉函數

這道題之前沒有看數論函數的時候搞懂了,想到直接用歐拉函數做,現在再來看第一個想法就是這不是莫比烏斯反演嘛. 但還是能用簡單數論知識直接做出來的還是盡量做簡單一點. 兩種方法想到后都寫的差不多對了,都爆long long 了.萬惡的long long .實在是煩.切記切記,只要是乘積,或…

epoll用法整理 實現回聲服務端

http://blog.csdn.net/chenxun_2010/article/details/504934811、epoll是什么&#xff1f; epoll是當前在Linux下開發大規模并發網絡程序的熱門人選&#xff0c;epoll 在Linux2.6內核中正式引入&#xff0c;和select相似&#xff0c;都是I/O多路復用(IO multiplexing)技術。 Li…

HDU3430-擴展中國剩余定理

剛開始一直把題意看錯了。。。體測完智商急劇下降 正確理解題意以后自己寫一直wa&#xff0c;而且并不知道是哪里的問題&#xff0c;在網上看了一下其他人寫的改了改自己的就過了&#xff0c;可是之前的還是不知道為什么不對。 題意大概就是有一個置換群&#xff0c;問運算多…

linux shell編程多線程和wait命令學習

http://blog.csdn.net/shuanghujushi/article/details/38186303最近在使用shell做一些部署工作&#xff0c;在使用過程中&#xff0c;效率一直不高。想提高效率&#xff0c;經過分析發現&#xff0c;并不是所有操作都是需要串行的&#xff0c;一些操作是可以進行并行操作的。經…

#ifndef的作用

#ifndef是一條預編譯指令&#xff0c;就是說實在編譯的時候就會運行的指令。這個指令的作用很簡單&#xff0c;就是字面意思&#xff0c;如果沒有定義的話&#xff0c;但是卻經常使用。 因為使用這個可以避免一個源文件中兩次兩次包含同一個文件&#xff0c;或者一個工程文件中…

C++中結構體和類的區別和聯系

最主要的不同點就是結構體的訪問權限為public而且不能改變&#xff0c;而類的訪問權限可以改變&#xff0c;public的類和結構體基本一樣。 繼承上同樣表現出這樣的特點&#xff0c;struct是public繼承的&#xff0c;而class是private繼承的&#xff0c;繼承的子類的訪問權限取…

poll函數實現多路復用

http://blog.csdn.net/xluren/article/details/8206371 結構體pollfd struct pollfd { int fd; //file descriptor short event; //event of interest on fd short reven; //event that occurred on fd } 每一個pollfd結構體指定了一個被監視的文件描述符&…

抽象類(純虛函數、虛函數)和虛基類(虛繼承)

C多態 C的多態包括靜態多態和動態多態&#xff0c;靜態多態包括函數重載和泛型編程&#xff0c;動態多態包括虛函數。靜態多態實在編譯期間就能確定&#xff0c;動態多態實直在程序運行時才能確定。 抽象類 虛函數 在默認情況下對函數成員調用實施的是靜態連編&#xff0c;…

socket通信之最簡單的socket通信

http://blog.csdn.net/xluren/article/details/8043484#t15 套接字有三種類型 流式套接字&#xff08;SOCK_STREAM&#xff09;&#xff0c;數據報套接字&#xff08;SOCK_DGRAM&#xff09;及原始套接字。 1.流式套接字提供面向連接、可靠的數據傳輸服務&#xff0c;數據按字節…

Java環境配置

自己安裝的時候按照一般的安裝方法先配置了JDK的環境&#xff0c;能夠成功顯示java版本后我在安裝eclipse的時候一直提示錯誤&#xff1a; Unfortunately the Java version needed to run Eclipse Installer couldn’t be found on your system. You need the following versio…

Linux I/O復用之select函數詳解

http://blog.csdn.net/y396397735/article/details/55004775 select函數的功能和調用順序 使用select函數時統一監視多個文件描述符的&#xff1a; 1、 是否存在套接字接收數據&#xff1f; 2、 無需阻塞傳輸數據的套接字有哪些? 3、 哪些套接字發生了異常&#xff1f; sel…

【Java學習筆記一】類和對象

面向對象程序設計的一個一個重要特點是&#xff1a;封裝性。 這里的封裝性有兩方面含義&#xff1a;一是將有關的數據和操作代碼封裝在一個對象中形成一個基本單位&#xff0c;各個對象之間相互獨立互不干擾&#xff0c;二是將對象中某些部分對外隱蔽&#xff0c;即隱蔽其內部細…

深入研究socket編程(3)——使用select函數編寫客戶端和服務器

http://blog.csdn.net/chenxun_2010/article/details/50488394 首先看原先《UNIX網絡編程——并發服務器&#xff08;TCP&#xff09;》的代碼&#xff0c;服務器代碼serv.c&#xff1a; [cpp] view plaincopy #include<stdio.h> #include<sys/types.h> #inclu…

Java簡單輸入輸出

不同于面向過程中有直接的輸入輸出函數&#xff0c;Java中的輸入輸出只能通過類來實現。 比較常見的一種是使用Scanner類 需要引入java.util包&#xff0c;即在文件開始加上語句import java.util.*;創建Scanner類對象&#xff0c;屬于標準輸入流。 例如Scanner snew Scanner(S…

Ubuntu安裝搭建Clion環境

嗚嗚嗚&#xff0c;太辛苦了&#xff0c;我終于安裝好這個了。 大概過程就是先在官網下載安裝包&#xff0c;然后解壓以后用終端移動到對應文件夾下運行clin.sh 運行完以后會有一些窗口&#xff0c;第一個選擇don’t~~&#xff0c;然后點擊ok 接受&#xff08;你可以不接受…

UNIX網絡編程——select函數的并發限制和 poll 函數應用舉例

http://blog.csdn.net/chenxun_2010/article/details/50489577 一、用select實現的并發服務器&#xff0c;能達到的并發數&#xff0c;受兩方面限制 1、一個進程能打開的最大文件描述符限制。這可以通過調整內核參數。可以通過ulimit -n來調整或者使用setrlimit函數設置&#x…

【Java學習筆記二】繼承和多態

與C不同的是&#xff0c;在Java中&#xff0c;一個類只能直接繼承另一個類&#xff0c;而不允許繼承多個類&#xff0c;這個新類稱為繼承類、派生類或者子類&#xff0c;而被繼承的類稱為基類或者父類。 繼承類能夠繼承基類的群不屬性和行為。 面向對象程序設計的三大特點為&…

使用poll實現的io多路復用服務端和客戶端

http://blog.csdn.net/robertkun/article/details/52269313 參考&#xff1a;http://www.cnblogs.com/Anker/p/3261006.html 使用poll實現的io多路復用服務端和客戶端。 客戶端通過子進程創建多個客戶端連接。 客戶端每隔1秒向服務端發送一個時間戳&#xff0c; 服務端接收到時…