Item 26: 避免對universal引用進行重載

来源:http://www.cnblogs.com/boydfd/archive/2016/03/27/5325404.html
-Advertisement-
Play Games

本文翻譯自《effective modern C++》,由於水平有限,故無法保證翻譯完全正確,歡迎指出錯誤。謝謝! 如果你需要寫一個以名字作為參數,並記錄下當前日期和時間的函數,在函數中還要把名字添加到全局的數據結構中去的話。你可能會想出看起來像這樣的一個函數: std::multiset name ...


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

如果你需要寫一個以名字作為參數,並記錄下當前日期和時間的函數,在函數中還要把名字添加到全局的數據結構中去的話。你可能會想出看起來像這樣的一個函數:

std::multiset<std::string> name;            // 全局數據結構

void logAndAdd(const std::string& name)
{
    auto now =                              // 得到當前時間
        std::chrono::system_clock::now();

    log(now, "logAndAdd");                  // 產生log條目

    names.emplace(name);                    // 把name添加到全局的數據結構中去
                                            // 關於emplace的信息,請看Item 42
}

這段代碼並非不合理,只是它可以變得更加有效率。考慮三個可能的調用:

std::string petName("Darla");

logAndAdd(petName);                     // 傳入一個std::string左值

logAndAdd(std::string("Persephone"));   // 傳入一個std::string右值

logAndAdd("Patty Dog");                 // 傳入字元串

在第一個調用中,logAndAdd的參數name被綁定到petName變數上了。在logAndAdd中,name最後被傳給names.emplace。因為name是一個左值,它是被拷貝到names中去的。因為被傳入logAndAdd的是左值(petName),所以我們沒有辦法避免這個拷貝。

在第二個調用中,name參數被綁定到一個右值上了(由“Persephone”字元串顯式創建的臨時變數---std::string)。name本身是一個左值,所以它是被拷貝到names中去的,但是我們知道,從原則上來說,它的值能被move到names中。在這個調用中,我們多做了一次拷貝,但是我們本應該通過一個move來實現的。

在第三個調用中,name參數再一次被綁定到了一個右值上,但是這次是由“Patty Dog”字元串隱式創建的臨時變數---std::string。就和第二種調用一樣,name試被拷貝到names中去的,但是在這種情況下,被傳給logAndAdd原始參數是字元串。如果把字元串直接傳給emplace的話,我們就不需要創建一個std::string臨時變數了。取而代之,在std::multiset內部,emplace將直接使用字元串來創建std::string對象。在第三種調用中,我們需要付出拷貝一個std::string的代價,但是我們甚至真的沒理由去付出一次move的代價,更別說是一次拷貝了。

我們能通過重寫logAndAdd來消除第二個以及第三個調用的低效性。我們使logAndAdd以一個universal引用(看Item24)為參數,並且根據Item 25,再把這個引用std::forward(轉發)給emplace。結果就是下麵的代碼了:

templace<typename T>
void logAndAdd(T& name)
{
    auto now = std::chrono::system_clock::now();
    log(now, "logAndAdd");
    names.emplace(std::forward<T>(name));
}

std::string petName("Darla");           // 和之前一樣

logAndAdd(petName);                     // 和之前一樣,拷貝左
                                        // 值到multiset中去

logAndAdd(std::string("Persephone"));   // 用move操作取代拷貝操作

logAndAdd("Patty Dog");                 // 在multiset內部創建
                                        // std::string,取代對
                                        // std::string臨時變數
                                        // 進行拷貝

萬歲!效率達到最優了!

如果這是故事的結尾,我能就此打住很自豪地離開了,但是我還沒告訴你客戶端並不是總能直接訪問logAndAdd所需要的name。一些客戶端只有一個索引值,這個索引值可以讓logAndAdd用來在表中查找相應的name。為了支持這樣的客戶端,logAndAdd被重載了:

std::string nameFromIdx(int idx);       // 返回對應於idx的name

void logAndAdd(int idx)                 // 新的重載
{
    auto now = std::chrono::system_clock::now();
    log(now, "logAndAdd");
    names.emplace(nameFromIdx(idx));
}

對於兩個重載版本的函數,調用的決議(決定調用哪個函數)結果就同我們所期待的一樣:

std::string petName("Darla");           // 和之前一樣

logAndAdd(petName);                     // 和之前一樣,這些函數
logAndAdd(std::string("Persephone"));   // 都調用T&&版本的重載
logAndAdd("Patty Dog");                 

logAndAdd(22);                          // 調用int版本的重載

事實上,決議結果能符合期待只有當你不期待太多時才行。假設一個客戶端有一個short類型的索引,並把它傳給了logAndAdd:

short nameIdx;
...                                     // 給nameIdx一個值

logAndAdd(nameIdx);                     // 錯誤!

最後一行的註釋不是很明確,所以讓我來解釋一下這裡發生了什麼。

這裡有兩個版本的logAndAdd。一個版本以universal引用為參數,它的T能被推導為short,因此產生了一個確切的匹配。以int為參數的版本只有在一次提升轉換(譯註:也就是類型轉換,從小精度數據轉換為高精度數據類型)後才能匹配成功。按照正常的重載函數決議規則,一個確切的匹配擊敗了需要提升轉換的匹配,所以universal引用重載被調用了。

在這個重載中,name參數被綁定到了傳入的short值。因此name就被std::forwarded到names(一個std::multiset<std::string>)的emplace成員函數,然後在內部又把name轉發給std::string的構造函數。但是std::string沒有一個以short為參數的構造函數,所以在logAndAdd調用中的multiset::emplace調用中的std::string構造函數的調用失敗了。這都是因為比起int版本的重載,universal引用版本的重載是short參數更好的匹配。

在C++中,以universal引用為參數的函數是最貪婪的函數。它們能實例化出大多數任何類型參數的準確匹配。(它無法匹配的一小部分類型將在Item 30中描述。)這就是為什麼把重載和universal引用結合起來使用是個糟糕的想法:比起開發者通常所能預想到的,universal引用版本的重載使得參數類型失效的數量要多很多。

一個簡單的讓事情變複雜的辦法就是寫一個完美轉發的構造函數。一個對logAndAdd例子中的小改動能說明這個問題。比起寫一個以std::string或索引(能用來查看一個std::string)為參數的函數,我們不如寫一個能做同樣事情的Person類:

class Person {
publci:
    template<typename T>
    explicit Person(T&& n)          // 完美轉發的構造函數
    : name(std::forward<T>(n)) {}   // 初始化數據成員

    explicit Person(int idx)        // int構造函數
    : name(nameFromIdx(idx)) {}
    …
private:
    std::string name;
};

就和logAndAdd中的情況一樣,傳一個除了int外的整形類型(比如,std::size_t, short, long)將不會調用int版本的構造函數,而是調用universal引用版本的構造函數,然後這將導致編譯失敗。但是這裡的問題更加糟糕,因為除了我們能看到的以外,這裡還有別的重載出現在Person中。Item 17解釋了在適當的條件下,C++將同時產生拷貝和move構造函數,即使類中包含一個能實例化出同拷貝或move構造函數同樣函數簽名的模板構造函數,它還是會這麼做。因此,如果Person的拷貝和move構造函數被產生出來了,Person實際上看起來應該像是這樣:

class Person {
public:
    template<typename T>                    
    explicit Person(T&& n)
    : name(std::forward<T>(n)) {}


    explicit Person(int idx); 
    
    Person(const Person& rhs);      // 拷貝構造函數
                                    // (編譯器產生的)

    Person(Person&& rhs);           // move構造函數
    …                               // (編譯器產生的)
};

只有你花了大量的時間在編譯期和寫編譯器上,你才會忘記以人類的想法去思考這個問題,知道這將導致一個很直觀的行為:

Person p("Nancy");

auto cloneOfP(p);               // 從p創建一個新的Person
                                // 這將無法通過編譯!

在這裡我們試著從另外一個Person創建一個Person,這看起來就拷貝構造函數的情況是一樣的。(p是一個左值,所以我們能不去考慮“拷貝”可能通過move操作來完成)。但是這段代碼不能調用拷貝構造函數。它將調用完美轉發構造函數。然後這個函數將試著用一個Person對象(p)來初始化Person的std::string數據成員。std::string沒有以Person為參數的構造函數,因此你的編譯器將憤怒地舉手投降,可能會用一大串無法理解的錯誤消息來表達他們的不快。

“為什麼?”你可能很奇怪,“難道完美轉發構造函數取代拷貝構造函數被調用了?可是我們在用另外一個Person來初始化這個Person啊!”。我們確實是這麼做的,但是編譯器卻是誓死維護C++規則的,然後和這裡相關的規則是對於重載函數,應該調用哪個函數的規則。

編譯器的理由如下:cloneOfP被用一個非const左值(p)初始化,並且這意味著模板化的構造函數能實例化出一個以非const左值類型為參數的Person構造函數。在這個實例化過後,Person類看起來像這樣:

class Person {
public:
    explicit Person(Person& n)              // 從完美轉發構造函數
    : name(std::forward<Person&>(n)) {}     // 實例化出來的構造函數
                                            
    explicit Person(int idx);               // 和之前一樣

    Person(const Person& rhs);              // 拷貝構造函數
    ...                                     // (編譯器產生的)

};

在語句

auto cloneOfP(p);

中,p既能被傳給拷貝構造函數也能被傳給實例化的模板。調用拷貝構造函數將需要把const加到p上去來匹配拷貝構造函數的參數類型,但是調用實例化的模板不需要這樣的條件。因此產生自模板的版本是更佳的匹配,所以編譯器做了它們該做的事:調用更匹配的函數。因此,“拷貝”一個Person類型的非const左值會被完美轉發構造函數處理,而不是拷貝構造函數。

如果我們稍微改變一下例子,使得要被拷貝的對象是const的,我們將得到一個完全不同的結果:

const Person cp("Nancy");       // 對象現在是const的

auto cloneOfP(cp);              // 調用拷貝構造函數!

因為被拷貝的對象現在是const的,它完全匹配上拷貝構造函數的參數。模板化的構造函數能被實例化成有同樣簽名的函數,

class Person {
public:
    explicit Person(const Person& n);       //從模板實例化出來

    Person(const Person& rhs);              // 拷貝構造函數
                                            // (編譯器產生的)
    ...
};

但是這不要緊,因為C++的“重載決議”規則中有一條就是當模板實例和一個非模板函數(也就是一個“正常的”函數)都能很好地匹配一個函數調用時,正常的函數是更好的選擇。因此拷貝構造函數(一個正常的函數)用相同的函數簽名打敗了被實例化的模板。

(如果你好奇為什麼當編譯器能用模板構造函數實例化出同拷貝構造函數一樣的簽名時,它們還是會產生一個拷貝構造函數,請複習Item 17。)

當繼承介入其中時,完美轉發構造函數、編譯器產生的拷貝和move構造函數之間的關係將變得更加扭曲。尤其是傳統的派生類對於拷貝和move操作的實現將變得很奇怪,讓我們來看一下:

class SpecialPerson: public Person {
public:
    SpecialPerson(const SpecialPerson& rhs)     // 拷貝構造函數,調用
    : Person(rhs)                               // 基類的轉發構造函數
    { … }                                       

    SpecialPerson(SpecialPerson&& rhs)          // move構造函數,調用
    : Person(std::move(rhs))                    // 基類的轉發構造函數
    { … }                                       
};

就像註釋標明的那樣,派生的類拷貝和move構造函數沒有調用基類的拷貝和move構造函數,它們調用基類的完美轉發構造函數!為了理解為什麼,註意派生類函數傳給基類的參數類型是SpecialPerson類型,然後產生了一個模板實例,這個模板實例成為了Person類構造函數的重載決議結果。最後,代碼無法編譯,因為std::string構造函數沒有以SpecialPerson為參數的版本。

我希望現在我已經讓你確信,對於universal引用參數進行重載是你應該儘可能去避免的事情。但是如果重載universal引用是一個糟糕的想法的話,那麼如果你需要一個函數來轉發不同的參數類型,並且需要對一小部分的參數類型做特殊的事情,你該怎麼做呢?事實上這裡有很多方式來完成這件事,我將花一整個Item來講解它們,就在Item 27中。下一章就是了,繼續讀下去,你會碰到的。

            你要記住的事
  • 重載universal引用常常導致universal引用版本的重載被調用的頻率超過你的預期。
  • 完美轉發構造函數是最有問題的,因為比起非const左值,它們常常是更好的匹配,並且它們會劫持派生類調用基類的拷貝和move構造函數。

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

-Advertisement-
Play Games
更多相關文章
  • 配置struts2常量的三種方法: 1.使用struts.xml中的<constant>元素配置 eg:<constant name="struts.action.extension" value="true"> 2.在struts.properties文件中配置 eg:struts.action. ...
  • 註釋 編譯器會用空格代替代碼中原來的註釋,並先於預處理指令執行/*…*/ 這種形式的註釋不能嵌套只要斜杠(/)和星號(*)之間沒有空格,都會被當作註釋的開始。例如這樣:y = x/*p; \ 是一個接續符,表示斷行。編譯器會將反斜杠剔除掉,跟在反斜杠後面的字元自動接續到前一行。但是註意:反斜杠之後不 ...
  • 本節作業: 熟練使用類和模塊,寫一個交互性強、有衝突的程式。 故本次寫了一個文字回合制的PK游戲,系統主程式為根目錄下的:game_menu.py 1. 系統功能模塊: 第六天的作業:文字游戲程式是在python3.4環境下開發,在python2.7環境下大同小異,主要功能模塊如下圖: 2. 系統目 ...
  • ...
  • 一:取字元串中第幾個字元 print "Hello"[0] 表示輸出字元串中第一個字元print "Hello"[-1] 表示輸出字元串中最後一個字元 二:字元串分割 print "Hello"[1:3] #第一個參數表示原來字元串中的下表#第二個闡述表示分割後剩下的字元串的第一個字元 在 原來字元 ...
  • 1.Tomcat記憶體溢出的原因 生產環境中Tomcat記憶體設置不好很容易出現記憶體溢出。造成記憶體溢出是不一樣的,當然處理方式也不一樣。 這裡根據平時遇到的情況和相關資料進行一個總結。常見的一般會有下麵三種情況: OutOfMemoryError: Java heap space OutOfMemory ...
  • This is a test 1 2 3 4 1 2 3 4 Code test ...
  • python多繼承,剛開始我是表示驚訝的,畢竟學的php,哪來的多繼承?頂多也就是利用介面模擬多繼承後者使用反射機制實現。那麼還是來看看python的強大吧 1 首先,Python的類繼承了多個類,那麼其尋找方法的方式有兩種,分別是:深度優先(經典類)和廣度優先(新式類) 2 共有與私有成員(很多的 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...