item 23: 理解template類型的推導

来源:http://www.cnblogs.com/boydfd/archive/2016/02/05/5182743.html
-Advertisement-
Play Games

本文翻譯自《effective modern C++》,由於水平有限,故無法保證翻譯完全正確,歡迎指出錯誤。謝謝! 根據std::move和std::forward不能做什麼來熟悉它們是一個好辦法。std::move沒有move任何東西,std::forward沒有轉發任何東西。在運行期,它們沒有做


本文翻譯自《effective modern C++》,由於水平有限,故無法保證翻譯完全正確,歡迎指出錯誤。謝謝!

根據std::move和std::forward不能做什麼來熟悉它們是一個好辦法。std::move沒有move任何東西,std::forward沒有轉發任何東西。在運行期,它們沒有做任何事情。它們沒有產生需要執行的代碼,一byte都沒有。

std::move和std::forward只不過就是執行cast的兩個函數(實際上是函數模板)。std::move無條件地把它的參數轉換成一個右值,而std::forward只在特定條件滿足的情況下執行這個轉換。就是這樣了,我的解釋又引申出一系列的新問題,但是,基本上來說,上面說的就是全部內容了。

為了讓內容更加形象,這裡給出C++11中std::move實現的一個例子。它沒有完全遵循標準的細節,但是很接近了。

template<typename T>                                //在命名空間std中
typename remove_reference<T>::type&&
move(T&& param)
{
    using ReturnType =                              //別名聲明
        typename remove_reference<T>::type&&;       //看Item 9

    return static_cast<ReturnType>(param);
}

我已經幫你把代碼的兩個部分高亮(move和static_cast)顯示了。一個是函數的名字,因為返回值類型挺複雜的,我不想讓你在這複雜的地方浪費時間。另一個地方是包括了這個函數的本質(cast)。就像你看到的那樣,std::move需要一個對象的引用(準確地說是一個universal引用,看Item 24),並且返回同一個對象的引用。

函數返回值類型的“&&”部分暗示了std::move返回一個右值引用,但是,就像Item 28解釋的那樣,如果類型T恰好是左值引用,T&&將成為一個左值引用。為了防止這樣的事情發生,type trait(看Item 9)std::remove_reference被用在T上了,因此能保證把“&&”加在不是引用的類型上。這樣能保證讓std::move確切地返回一個右值引用,並且這是很重要的,因為由函數返回的右值引用是一個右值。因此,std::move所做的所有事情就是轉換它的參數為一個右值。

說句題外話,在C++14中std::move能被實現得更簡便一些。多虧了函數返回值類型推導(看Item 3)以及標準庫的別名模板std::remove_reference_t(看Item 9),std::move能被寫成這樣:

template<typename T>
decltype(auto) move(T&& param)
{
    using ReturnType = remove_reference_t<T>&&;
    return static_cast<ReturnType>(param);
}

看上去更簡單了,不是嗎?

因為std::move值只轉換它的參數為右值,這裡有一些更好的名字,比如說rvalue_cast。儘管如此,我們仍然使用std::move作為它的名字,所以記住std::move做了什麼和沒做什麼很重要。它做的是轉換,沒有做move。

當然了,右值是move的候選人,所以把std::move應用在對象上能告訴編譯器,這個對象是有資格被move的。這也就是為什麼std::move有這樣的名字:能讓指定的對象更容易被move。

事實上,右值是move的唯一候選人。假設你寫了一個代表註釋的類。這個類的構造函數有一個std::string的參數,並且它拷貝參數到一個數據成員中。根據Item 41中的信息,你聲明一個傳值的參數:

class Annotation {
public:
    explicit Annotation(std::string text);      // 要被拷貝的參數
                                                // 根據Item 41,聲明為傳值的
    ...
};

但是Annotation的構造函數只需要讀取text的值。它不需要修改它。為了符合歷史傳統(把const用在任何可以使用的地方),你修改了你的聲明,因此text成為了const的:

class Annotation {
public:
    explicit Annotation(const std::string text)
    ...
};

為了在拷貝text到數據成員的時候不把時間浪費在拷貝操作上,你保持Item 41的建議並且把std::move用在text上,因此產生了一個右值:

class Annotation {
public:
    explicit Annotation(const std::string text)
    :value(std::move(text))     // “move” text到value中去;這段代碼
    {...}                           //做的事情不像看上去那樣

    ...

private:
    std::string value;
};

代碼能夠編譯。代碼能夠鏈接。代碼能夠執行。代碼把數據成員value的值設為text的內容。這段代碼同完美的代碼(你所要的版本)之間的唯一不同之處就是text不是被move到value中去的,它是拷貝過去的。當熱,text通過std::move轉換成了一個右值,但是text被聲明為一個const std::string,所以在轉換之前,text是一個左值const std::string,然後轉換的結果就是一個右值const std::string,但是一直到最後,const屬性保留下來了。

考慮一下const對於編譯器決定調用哪個std::string構造函數有什麼影響。這裡有兩種可能:

class string {                      // std::string實際上是
public:                             // std::basic_string<char>的一個typedef
    ...
    string(const string& rhs);      // 拷貝構造函數
    string(string& rhs);            // move構造函數
    ...
};

在Annotation的構造函數的成員初始化列表中,std::move(text)的結果是一個const std::string的右值。這個右值不能傳給std::string的move構造函數,因為move構造函數只接受非const std::string的右值引用。但是,這個右值能被傳給拷貝構造函數,因為一個lvalue-reference-to-const(引用const的左值)能被綁定到一個const右值上去。因此即使text已經被轉化成了一個右值,成員初始化列表還是調用了std::string中的拷貝構造函數。這樣的行為本質上是為了維持const的正確性。一般把一個值move出去就相當於改動了這個對象,所以C++不允許const對象被傳給一個能改變其自身的函數(比如move構造函數)。

我們從這個例子中得到兩個教訓。第一,如果你想要讓一個對象能被move,就不要把這個對象聲明為const。在const對象上的move請求會被預設地轉換成拷貝操作。第二,std::move事實上沒有move任何東西,它甚至不能保證它轉換出來的對象能有資格被move。你唯一能知道的事情就是,把std::move用在一個對象之後,它變成了一個右值。

std::forward的情況和std::move相類似,但是std::move是無條件地把它的參數轉換成右值的,而std::forward只在確定條件下才這麼做。std::forward是一個有條件的轉換。為了理解它什麼時候轉換,什麼時候不轉換,回憶一下std::forward是怎麼使用的。最常見的情況就是,一個帶universal引用的參數被傳給另外一個參數:

void process(const Widget& lvalArg);            // 參數為左值
void process(Widget&& rvalArg);                 // 參數為右值

template<typename T>                            // 把參數傳給process
void logAndProcess(T&& param)                   // 的模板
{
    auto now =
        std::chrono::system_clock::now();       // 取得正確的時間

        makeLogEntry("Calling 'process'", now);
        process(std::forward<T>(param));
}

考慮一下兩個logAndProcess調用,一個使用左值,另外一個使用右值:

Widget w;

logAndProcess(w);               // 用左值調用
logAndProcess(std::move(w));    // 用右值調用

在logAndProcess內部,參數param被傳給process函數。process重載了左值和右值兩個版本。當我們用左值調用logAndProcess的時候,我們自然是希望這個左值作為一個左值被轉發給process,然後當我們使用右值調用logAndProcess時,我們希望右值版本的process被調用。

但是param就和所有的函數參數一樣,是一個左值。因此在logAndProcess內部總是調用左值版本的process。為了防止這樣的事情發生,我們需要一種機制來讓param在它被一個右值初始化(傳給logAndProcess的參數)的時候轉換成右值。這正好就是std::forward做的事情。這也就是為什麼std::forward是一個條件轉換:它只把用右值初始化的參數轉換成右值。

你可能會奇怪std::forward怎麼知道他的參數是不是用右值初始化的。舉個例子吧,在上面的代碼中,std::forward怎麼會知道param是被左值還是右值初始化的呢?簡單來說就是這個信息被包含在logAndProcess的模板參數T中了。這個參數被傳給了std::forward,這樣就讓std::forward得知了這個信息。它具體怎麼工作的細節請參考Item 28。

考慮到std::move和std::forward都被歸結為轉換,不同之處就是std::move總是執行轉換,但是std::forward只在有些情況下執行轉換,你可能會問我們是不是可以去掉std::move並且在所有的地方都只使用std::forward。從技術的角度來看,回答是可以:std::forward能做到所有的事情。std::move不是必須的。當然,這兩個函數函數都不是“必須的”,因為我們能在使用的地方寫cast,但是我希望我們能同意它們是必須的函數,好吧,真是令人心煩的事。

std::move的優點是方便,減少相似的錯誤,並且更加清晰。考慮一個類,對於這個類我們想要記錄它的move構造函數被調用了多少次。一個能在move構造的時候自增的static計數器就是我們需要的東西了。假設這個類中唯一的非static數據是一個std::string,這裡給出通常的辦法(也就是使用std::move)來實現move構造函數:

class Widget {
public:
    Widget(Widget&& rhs)
    : s(std::move(rhs.s))
    { ++moveCtorCalls;}
}

...

private:

 static std::size_t moveCtorCalls;
 std::string s;
};

為了用std::forward來實現相同的行為,代碼看起來像是這樣的:

class Widget {
public:
    Widget(Wdiget&& rhs)                    //不常見,以及不受歡迎的實現
    : s(std::forward<std::string>(rhs.s))
    //譯註:為什麼是std::string請看Item 1,用右值傳入std::string&& str的話
    //推導的結果T就是std::string,用左值傳入,則推導的結果T會是std::string&
    //然後這個T就需要拿來用作forward的模板類型參數了。
    //詳細的解釋可以參考Item28
    { ++moveCtorCalls; }
};

首先註意std::move只需要一個函數參數(rhs.s),而std::forward卻需要一個函數參數(rhs.s)以及一個模板類型參數(std::string)。然後註意一下我們傳給std::forward的類型應該是一個非引用類型,因為我們約定好傳入右值的時候要這麼編碼(傳入一個非引用類型,看Item 28)。也就是說,這意味著std::move需要輸入的東西比std::forward更少,還有,它去掉了我們傳入的參數是右值時的麻煩(記住類型參數的編碼)。它也消除了我們傳入錯誤類型(比如,std::string&,這會導致數據成員用拷貝構造函數來替換move構造函數)的可能。

更加重要的是,使用std::move表示無條件轉換到一個右值,然後使用std::forward表示只有引用的是右值時才轉換到右值。這是兩種非常不同的行為。第一個常常執行move操作,但是第二個只是傳遞(轉發)一個對象給另外一個函數並且保留它原始的左值屬性或右值屬性。因為這些行為如此地不同,所以我們使用兩個函數(以及函數名)來區分它們是很好的主意。

            你要記住的事
  • std::move執行到右值的無條件轉換。就其本身而言,它沒有move任何東西。
  • std::forward只有在它的參數綁定到一個右值上的時候,它才轉換它的參數到一個右值。
  • std::move和std::forward在運行期都沒有做任何事情。

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 2.0自定義菜單管理 ①介面說明: 微信服務號聊天視窗下麵的菜單項(有的公眾號有啟用有的則沒有),這個可以在編輯模式簡單配置,也可以在開發模式代碼配置。微信公眾平臺開發者文檔:微信公眾號開發平臺創建自定義菜單,可以看到創建菜單的一些註意事項,下麵的使用網頁調試工具調試該介面,只是調試介面是否可以正常
  • 本篇文章提供了一個開源JavaScript庫,它提供了給AJAX應用程式中添加書簽和會退按鈕的功能。在學習完這個教程後,開發者將能夠對開發AJAX應用碰到的問題獲得一個解決方案,這個特性甚至Google Maps 和 Gmail 現在都不提供:提供一個強大的,可用的書簽和前進回退按鈕,如同其他的WE
  • 環境說明:Vistual Studio 2013 MVC 4.0 其實關於ASP.NET MVC Area使用的基礎知識可以參考 http://www.cnblogs.com/willick/p/3331519.html 這篇軟文. Global.asax 中的 Application_Start
  • 分類:C#、Android、百度地圖應用; 日期:2016-02-04 一、簡介 百度地圖SDK為廣大開發者開放了OpenGL繪製介面,幫助開發者在地圖上實現更靈活的樣式繪製,豐富地圖使用效果體驗。 二、運行截圖 簡介:介紹如何使用OpenGL在地圖上實現自定義繪製。 詳述: (1)利用OpenGL...
  • .net coreclr 已經發佈RC1版本,安裝方法如下: 1.安裝DNVM,DNVM是.net運行時管理器,負責管理所有版本的.net運行時(.net framework、.net coreclr和Mono)。 C:\coreclr-demo> @powershell -NoProfile -E
  • 分類:C#、Android、百度地圖應用; 日期:2016-02-04 百度全景圖是一種實景地圖服務。為用戶提供城市、街道和其他環境的360度全景圖像,用戶可以通過該服務獲得如臨其境的地圖瀏覽體驗。 本示例演示如何利用百度Android全景SDK v2.2實現全景圖的檢索、顯示和交互功能,以便清晰方...
  • 如果想知道 AngularJs 通過WebAPI 下載Excel。請看下文,這裡僅提供了一種方案。 伺服器端代碼如下: protected HttpResponseMessage GenereateExcelMessage(HttpRequestMessage Request, string fil
  • 分類:C#、Android、百度地圖應用; 日期:2016-02-04 一、簡介 線路規劃支持以下功能: 公交信息查詢:可對公交詳細信息進行查詢; 公交換乘查詢:根據起、終點,查詢策略,進行線路規劃方案; 駕車線路規劃:提供不同策略,規劃駕車路線;(支持設置途經點) 步行路徑檢索:支持步行路徑的規劃...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...