Item 27: 明白什麼時候選擇重載,什麼時候選擇universal引用

博客已經遷移到這裏啦

Item 26已經解釋了,不管是對全局函數還是成員函數(尤其是構造函數)而言,對universal引用的重載會導致一系列的問題。到目前爲止,我也已經給出了好幾個例子,如果它能表現得和我們期待的一樣,這種重載也能很實用。此Item會探索如何讓這種重載能實現我們所需求的行爲。我們可以設計出避免對universal引用進行重載的實現,也可以通過限制參數的類型,來使得它們能夠匹配。

我們的討論將繼續建立在Item 26介紹的例子上。如果你最近沒有讀過那個Item,你需要在繼續此Item前複習一下它。

拋棄重載

Item 26中的第一個例子(logAndAdd)就是一個典型的例子,很多這樣的函數如果想要避免對universal引用進行重載,那隻要簡單地對即將重載的函數進行不同的命名即可。舉個例子,兩個logAndAdd重載能被分割成logAndAddName和logAndAddNameIdx。可惜的是,這個方法不能在第二個例子(Person構造函數)中工作,因爲構造函數的名字是由語言固定的。再說了,誰又想放棄重載呢?

通過const T&傳參數

另一個選擇是回到C++98,並且把pass-by-universal-reference(通過universal引用傳參數)替換成pass-by-lvalue-reference-to-const(通過const左值引用傳參數)。事實上,這是Item 26考慮的第一個方法(顯示在175頁)。這個辦法的缺點是它的效率無法達到最優。要知道,對於我們現在所知道的universal引用和重載來說,犧牲一些效率來保持事情的簡單性可能是一個很有吸引力的方案。

傳值

一個常常能讓你提升效率並且不增加複雜性的辦法是把傳引用的參數替換成傳值的參數。雖然這很不直觀,但這個設計遵守了Item 41的建議(當知道你需要拷貝一個對象時,直接通過傳值來傳遞它)。所以,對於它們怎麼工作以及它們有多高效的細節部分,我會推遲到Item 41再討論。在這,我只是給你看一下這個技術怎麼用在Person例子中去:

class Person {
public:
    explicit Person(std::string n)  // 替換T&&構造函數對於
    : name(std::move(n)) {}         // std::move的使用請看Item 41


    explicit Person(int idx)        // 和之前一樣    
    : name(nameFromIdx(idx)) {}
    ...


private:
    std::string name;
};

因爲std::string的構造函數接受類型爲整型的參數,所以所有傳給Person構造函數的int及類int(比如,std::size_t, short, long)的參數講調用int版本的重載。相似的,所有的std::string類型(以及那些可以用來創建一個std::string的參數,比如字符串”Ruth”)會被傳給以std::string爲參數的構造函數。因此對於調用者來說,這裏沒有意外發生。你能爭論說“我覺得有些人還是會感到奇怪,他們使用0或NULL來代表null指針,所以這會掉用int版本的重載”,但是這些人應該回到Item 8,然後再讀一次,直到他們覺得使用0或NULL來表示null指針會讓他們覺得可怕。

使用Tag dispatch(標籤分發)

不管是通過lvalue-reference-to-const傳遞還是傳值的方式來支持完美轉發。如果使用universal引用的動機是完美轉發的話,我們沒有其他的選擇。我們還是不想拋棄重載。所以如果我們不想拋棄重載,也不想拋棄universal引用的話,我們怎麼才能避免對universal引用進行重載呢?

事實上沒有這麼困難。重載函數的調用是這樣的:依次查看每個重載函數的參數(形參)以及調用點的參數(實參),然後選擇最匹配的重載函數(匹配上所有的形參和實參)。一個universal引用參數通常提供一個格外的匹配,使得不管傳入的是什麼,都能匹配上,但是如果universal引用只是參數列表的一部分,這個參數列表還包含其他不是universal引用的參數,那麼,即使不考慮universal引用,非universal引用參數就足夠我們造成不匹配了。這就是tag dispatch方法背後的基礎,一個例子會讓之前的描述更加好理解。

我們把tag dispatch永在logAndAdd177頁的例子上去。爲了避免你分神去找,這裏給出那個例子的代碼:

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


template<typename T>                                // 創建log的實體並把它加 
void logAndAdd(T&& name)                            // 到全局的數據結構中去
{
    auto now = std::chrono::system_clock::now();
    log(now, "logAndAdd");
    names.emplace(std::forward<T>(name));
}

如果只看它自己,這個函數工作得很好,但是當我們加入以int(用來通過索引查找對象)類型爲參數的重載函數時,我們就回到了Item 26所遇到的問題。這個Item的目的是避免這個問題。比起添加一個重載,我們重新實現logAndAdd,讓它作爲其它兩個函數(一個爲了整型類型,一個爲了其它類型)的代理。logAndAdd它自己將同時接受整型和非整型類型的所有參數。

真正做事情的兩個函數將被命名爲logAndAddImpl,也就是我們將重載它們。其中一個將以universal引用爲參數,所以我們將同時擁有重載和universal引用。但是,每個函數也將攜帶第二個參數,一個用來指示傳入的參數是不是整型的參數。這第二個參數將防止我們落入Item 26所描述的陷阱中去,因爲我們將讓第二個參數成爲決定哪個重載將被選擇的要素。

是的,我知道,“廢話少說,讓我看代碼!”,沒問題。這裏給出更新後的logAndAdd,這是一個幾乎正確的版本:

template<typename T>
void logAndAdd(T&& name)
{
    logAndAddImpl(std::forward<T>(name),
                  std::is_integral<T>());   // 不是很正確
}

這個函數把它的參數轉發給logAndAddImpl,但是它也傳了一個參數來指明第一個參數的類型(T)是不是一個整型。至少,這是我們假設要做到的。對於是右值類型的整型參數,它也做到了該做的事情。但是,就像Item 28解釋的那樣,如果一個左值參數被傳給name(universal引用),T的類型將被推導爲左值引用。所以如果int類型的左值被傳入logAndAdd,T將被推導爲int&。它不是int類型,因爲引用不是整型。這意味着,對於任何左值類型,即使參數真的是一個整型,std::is_integral得到的也都會是false。

認識問題的過程就相當於在解決問題了,因爲便利的C++標準庫已經有type trait(看Item 9)了,std::remove_reference既做了它的名字要做的事情,也做了我們所希望的事情:把一個類型的引用屬性給去掉。因此logAndAdd的正確寫法是:

template<typename T>
void logAndAdd(T&& name)
{
    logAndAddImpl(
        std::forward<T>(name),
        std::is_integral<typename std::remove_reference<T>::type()
    );
}

這裏還有個小技巧(在C++14中,你能通過使用std::remove_reference_t<T>而少打幾個字,詳細內容可以看Item 9)

處理完這些household,我們能把我們的注意力轉移到函數在被調用的時候了,就是logAndAddImpl。這裏有兩個重載,第一個重載只能用在非整型變量上(也就是std::is_integral<typename std::remove_reference<T>::type>會返回false的類型):

template<typename T>
void logAndAddImpl(T&& name, std::false_type)       // 非整型參數:把它添加
{                                                   // 到全局的數據結構中去
    auto now = std::chrono::system_clock::now();
    log(now, "logAndAdd");
    names.emplace(std::forward<T>(name));
}

一旦你理解了隱藏在std::false_type背後的原理,這樣的代碼就顯得很直接了。概念上來講logAndAdd傳了一個布爾值給logAndAddImpl,用這個布爾值來標明傳給logAndAdd的類型是不是一個整型,但是true和false是運行期的值,而我們需要靠重載決議(這是一個編譯期的場景)來選擇正確的logAndAddImpl。這意味着我們需要一個和true相一致的類型以及另外一個和false相一致的類型。這樣的需求足夠普遍,因此標準庫爲我們提供了std::true_typestd::false_type。通過logAndAdd傳給logAndAddImpl的參數是一個對象,如果T是整型的話,這個對象就繼承自std::true_type,否則這個對象就繼承自std::false_type。最後我們得到的結果就是,當調用logAndAdd時,只有當T不是整型時,我們實現的這個logAndAddImpl重載纔是重載決議的候選對象。

第二個重載則覆蓋了相反的情況:當T是一個整型時。在這種情況下,logAndAddImpl簡單地找到相應下標下的name,然後把name傳回給logAndAdd:
std::string nameFromIdx(int idx); // 和Item 26中一樣
void logAndAddImpl(int idx, std::true_type) // 整型參數:查找name,
{ // 並且用來調用logAndAdd
logAndAdd(nameFromIdx(idx));
}

通過讓logAndAddImpl查找相應的name,並且將那麼傳給logAndAdd(它將被std::forward給另一個logAndAddImpl重載),我們避免了將log的代碼同時放在兩個logAndAddImpl重載中。

在這種設計下,std::true_type類型和std::false_type類型被稱爲標籤,它的目的只是強制重載決議的結果變成我們想要的結果。注意,我們甚至都沒有給那個參數命名。它們在運行期沒有做任何事情,並且事實上我們希望編譯器會將標籤參數視爲無用參數,並且將它們從程序的執行畫面中優化掉(有的編譯器會這麼做,至少有時候會這麼做)。在logAndAdd裏面,對被重載函數的調用中,通過創造合適的標籤對象來“分發”工作給正確的重載。因此這種設計的名字叫做“標籤分發”。它是模板元編程的基石,並且,你看的現代C++庫的代碼越多,你就越可能遇到它。

就我們的目的而言,標籤分發是怎麼實現的並不是很重要,它使我們在不產生Item 26所描述的問題的前提下,將universal引用和重載結合起來了,這纔是最重要的。分發函數(longAndAdd)以一個不受限制的universal引用爲參數,但是這個函數不被重載。底層的實現函數(logAndAddImpl)會被重載,並且也以universal引用爲參數,但是它還帶一個便籤參數,並且標籤值被設計成不會有超過一個的重載會成爲候選匹配。這樣一來,它的標籤就決定了哪個重載會被調用。所以universal引用總是對它的參數產生確切匹配的事實就不重要了。

對於帶universal引用參數的模板進行約束

標籤分發的關鍵點是做爲客戶API的單個(不重載的)函數。這個函數把要完成的工作分發給實現函數。創建一個不重載的分發函數通常很簡單,但是就如Item 26中考慮的第二個問題一樣,對Person類(在178頁)的構造函數進行完美轉發就是一個例外。編譯器可能會產生拷貝和move構造函數,所以,即使你只寫了一個構造函數,並對它使用標籤轉發,一些對構造函數的調用可能會繞開標籤分發系統,被編譯器所產生的函數處理。

事實上,真正的問題不是編譯器產生的函數有時候會繞開標籤分發,而是它們沒有被傳過去。你幾乎總是想要拷貝構造的處理能做到拷貝一份傳來參數的左值,但是就如Item 26描述的那樣,提供一個以universal引用爲參數的構造函數會使得,當拷貝一個非const左值時,universal引用版本的構造函數(而不是拷貝構造函數)會被調用。那個Item同時也解釋了當一個基類聲明瞭一個完美轉發構造函數時,如果它的派生類以傳統方式(將參數傳給基類)實現了自己拷貝或move構造函數,即使正確的行爲應該是調用基類的拷貝或move構造函數,最後的結果也還是調用完美轉發構造函數。

對於這些情況,帶universal引用參數的函數比你想象得更加貪婪,但是要做爲一個單分發函數卻不夠貪婪(譯註:因爲分發函數需要接受所有類型的參數,可是我們的函數不包括拷貝構造函數和move構造函數),因此標籤分發不是你要找的機制。你需要一個不同的技術,這個技術能讓你區分以下的情況:做爲函數模板的一部分,universal引用是否被允許使用。我的朋友啊,你需要的是std::enable_if

std::enable_if讓你能強制編譯器表現得好像一些特殊的模板不存在一樣。這樣的模板被稱爲無效的。通常情況下,所有的模板都是有效的,但是使用了std::enable_if後,只有滿足std::enable_if限定條件的模板纔是有效的。在我們的場景下,對於Person構造函數,我們只想讓被傳入的參數類型不是Person時進行完美轉發。如果傳入的類型是Person,我們想要讓完美轉發構造函數失效(也就是讓編譯器忽略它),因爲這會使得,當我們想用其他Person對象初始化一個Person對象時,類的拷貝和move構造函數能處理這些調用,

想要表達這個想法不是很困難,但是我們卻不知道具體語法,尤其是你之前沒見過的話,所以我會簡單地向你介紹一下。std::enable_if的條件部分還不是很明確,所以我們會從它開始。在我們給出的Person中有一個完美轉發構造函數的聲明,和例子一樣,std::enable_if用起來很簡單。我只給你展現了這個構造函數的聲明,因爲std::enable_if在函數的實現中沒有作用。實現還是和Item 26中的實現一樣。

calss Person {
public:
    template<typename T,
             typename = typename std::enable_if<condition>::type>
    explicit Person(T&& n);
    ...
};

爲了理解(typename = typename std::enable_if::type)到底做了什麼,我很遺憾地建議你參考別的材料,因爲我需要花一段時間才能解釋這個細節,但是已經沒有足夠的空間讓我在這本書中解釋它了(在你的搜索中,搜索“SFINAE”和std::enable_if是一樣的,因爲SFINAE就是讓std::enable_if工作的底層技術)這裏,我想要集中在條件表達式中,它能控制什麼樣的構造函數是有效的。

我們想要明確的條件是T不是Person,也就是說,只有當T是除了Person以外的類型時,模板化的構造函數纔是有效的。多虧了type trait(std::is_same),我們能判斷兩個類型是否相同,看起來,我們想要的條件是!std::is_same<Person, T>::value。(注意,表達式最前面的”!”。我們想要的是Person和T是不同的)這和我們想要的很接近了,但是還有點不對,因爲,就像Item 28解釋的那樣,用左值初始化時,對universal引用的類型推導總是一個左值引用。這意味着像下面這樣的代碼,

Person p("Nancy");


auto cloneOfP(p);       // 從左值初始化

在universal構造函數中,類型T將被推導成Person&。類型Person和Person&不相同,因此std::is_same的結果將反應以下的事實:std::is_same<Person, Person&>::value是false

如果我們考慮地更精確一些,我們會意識到,當我們在說“Person的模板化構造函數只有在T不是Person時纔有效”時,對於T,我們會想要忽略:

  • 它是否是一個引用。對於決定universal引用構造函數是否是有效的,Person,Person
    &,以及Person&&都應該和Person相同。
  • 它是否是const或volatile的。就我們而言,一個const Person和一個volatile Person以及一個const volatile Person和一個Person都是一樣的。

這意味着,在檢查T和Person是否相同之前,我們需要一個辦法來去除T的引用,const,volatile屬性。標準庫再一次用type trait的形式給了我們我們所需要的東西。這次的trait是std::decay(decay是退化的意思)。除了引用和CV限定符(也就是const或volatile限定符)被移除以外,std::decay::type和T是一樣的。(我捏造了事實,因爲就如它的name所表示的,std::decay也會讓數組和函數退化成指針(看Item 1),但是就這次討論的目的而言,std::decay表現得和我描述的一樣)那麼,我們用來控制我們的構造函數是否有效的條件就變成了:

!std::is_same<Person, typename std::decay<T>::type>::value

也就是,在忽略引用或CV限定符情況下,Person和類型T不相同。(就如Item 9解釋的那樣,std::decay前面的“typename”是必須的,因爲std::decay::type的類型依賴於模板參數T)

將條件插入前面std::enable_if不明確的部分,並且格式化一下,讓結果的結構更清晰,於是對Person的完美轉發構造函數就產生了這樣的聲明式:

class Person {
public:
    template<
        typename T,
        typename = typename std::enable_if<
                     !std::is_same<Person,
                                   typename std::decay<T>::type
                                  >::value
                    >::type
    >
    explicit Person(T&& n);


    ...


};

如果你從來沒有看過上面這樣的代碼,感謝主。我將這種設計保留到最後是有一個原因的。當你能使用其它的機制來避免universal引用同重載的混合時(你幾乎總是能這麼做),你應該避免這麼做。但是一旦你習慣了功能性的語法以及大量的尖括號,其實也不算很糟糕。此外,這給了你一直追求的行爲。上面給出的聲明式,從另外一個Person(不管是左值還是右值,const還是非const,volatile還是非volatile)構造Person時,永遠都不會調用以universal引用爲參數的構造函數。

成功了,對吧?我們做到了!

噢,不。先不要急着慶祝。我們還沒有解決Item 26中最後提的一點。我們需要解決它。

假設一個類從Person繼承,並用傳統的方式實現了拷貝和move構造函數:

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


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


    ...
};

包括註釋,這段代碼和Item 26(在206頁)給出的代碼一模一樣,它還需要我們調整。當我們拷貝或move一個SpecialPerson對象時,我們想要使用基類的拷貝或move構造函數,來拷貝或move它的基類部分,但是在這些函數中,我們傳入了一個SpecialPerson對象給基類對象,並且因爲SpecialPerson和Person的類型不一致(即使在使用std::decay之後也不一致),所以基類中的universal引用構造函數是有效的,並且它很樂意實例化出一個能對SpecialPerson參數精確匹配的函數。這樣的精確匹配比起從派生類到基類的轉換(要將SpecialPerson對象綁定到Person的拷貝和move構造函數上的Person參數時,這種轉換時必須的)更合適,所以對於我們現在擁有的代碼,move和拷貝SpecialPerson對象將調用Person的完美轉發構造函數來拷貝或move它們的基類部分!又一次回到了Item 26中的問題。

派生類在實現拷貝和move構造函數的時候只是遵循了通常的規則,所以要解決這個問題,必須把目光集中在基類中,尤其是判斷Person的universal引用是否有效的條件判斷上。現在我們知道,在模板化的構造函數中,我們不是想讓除了Person類型以外的參數有效,而是想讓除了Person以及從Person繼承的類型以外的參數有效。討人厭的繼承!

現在聽到“標準type traits中有一個traits能判斷一個類型是否從另一個類型繼承”你應該不會感到驚訝了吧。它叫做std::is_base_of。如果T2從T1那繼承,那麼std::is_base_of<T1, T2>::value爲true。類型自己被認爲是從自己繼承的,所以std::is_base_of<T, T>::value爲true。這很方便,因爲我們想要修改我們的控制條件,使得Person的完美轉發構造函數滿足以下條件:當T去除它的引用以及CV限定符時,它要麼是Person,要麼是從Person繼承的。使用std::is_base_of來代替std::is_same就是我們想要的:

class Person {
public:
    template<
      typename T,
      typename = typename std::enable_if<
                   !std::is_base_of<Person,
                                    typename std::decay<T>::type>
                                    ::value
                  >::type
     >
     explicit Person(T&& n);


     ...
};

現在我們終於做到了。我們提供的是C++11的代碼。如果我們使用C++14,代碼同樣能工作,但是我們能使用別名template來避免討厭的“typename”和“::type”,它們是std::enable_if_tstd::decay_t。因此會產生這樣更加令人舒適的代碼:

class Person {                                      // C++14
public:
    template<typename T,
             typenmae = std::enable_if_t<           // 更少的代碼
               !std::is_base_of<Person,
                                std::decay_t<T>     // 更少的代碼
                                >::value
            >                                       // 更少的代碼
    >
    explicit Person(T&& n);


    ...


};

好吧,我承認:我撒謊了。我們還沒完。但是很接近正確答案了。真的非常接近了!

我們已經看過怎麼使用std::enable_if來選擇讓Person的universal引用構造函數的一部分的參數失效,使得這些參數能被類的拷貝和move構造函數調用,但是我們還沒看過怎麼將其用在區分整型和非整型上。畢竟,這是我們最初的目標;這個構造函數的歧義問題就是我們一拖再拖的事情。

我們要做的所有事情就是:(1)添加一個Person構造函數的重載來處理整型參數,(2)進一步限制模板構造函數,使得它對整型參數失效。將我們討論過的東西全部倒入鍋中,兼以慢火烘烤,然後就可以盡情享受成功的芬芳了:

class Person {
public:
    template<
      typename T,
      typename = std::enable_if_t<
        !std::is_base_of<Person, std::decay_t<T>>::value
        &&
        !std::is_integral<std::remove_reference_t<T>>::value
      >
    >
    explicit Person(T&& n)      // std::string以及能被轉換成
    : name(std::foward<T>(n))   // std::string的構造函數
    { ... }


    explicit Person(int idx)    // 整型的構造函數
    : name(nameFromIdx(idx))
    { ... }


    ...                         // 拷貝和move構造函數等


private:
    std::string name;
};

看!多麼漂亮的東西!好吧,漂亮也許只是那些模板元編程者宣稱的,但是事實上,這個方法不僅完成了工作,它還是帶着獨特的沉着來完成的。因爲它使用了完美轉發,它提供了最高的效率,因爲他控制了universal引用和重載的結合,而不是禁止它,這個技術能被用在重載是無法避免的情況下(比如構造函數)。

權衡

這個Item最先考慮的三種技術(禁止重載,pass by const T&,pass by value)明確了將要調用的函數的每個參數類型。之後的兩種技術(標籤轉發,限制模板的資格)使用了完美轉發,因此不明確參數的類型。這種根本上的不同決策(是否明確類型)有着很大的影響。

做爲一種規則,完美轉發更加高效,因爲它避免了僅僅是爲了符合聲明式上的參數類型而創建臨時對象,在Person構造函數的例子中,完美轉發允許一個像”Nancy”一樣的字符串被轉發給std::string(Person中的name)的構造函數。而不使用完美轉發的技術必須從字符串創建臨時的std::string對象,這樣才能符合Person構造函數明確的參數類型。

但是完美轉發有缺點。一個是有些參數無法完美轉發,即使它們能被傳給以明確類型爲參數的函數。Item 30會探索那些完美轉發失敗的情況。

第二個問題是當客戶傳入一個不合法參數時,錯誤提示的可讀性。舉個例子,加入一個客戶創建Person對象的時候,傳入了一個由char16_t組成的字符串(這個類型在C++11中被介紹,它可以用來表示16bit的字符)來代替char(std::string是由它組成的):

Person p(u"Konrad Zuse")    // "Konrad Zuse"由const char16_t
                            // 的類型組成

由本Item最先提及的三個辦法來實現時,編譯器將會看到可用的構造函數只以int或std::string爲參數,然後它們就會產生一個或多或少很直接的錯誤提示,這個錯誤提示會解釋說無法從const char16_t[12]轉換爲int或std::string

但是用基於完美轉發的辦法來實現時,const char16_t數組會在沒有提示的情況下被綁定到構造函數的參數上去。然後它會被轉發到Person的std::string數據成員的構造函數上去,只有在這個時候,傳入的調用者(一個const char16_t)與需要的參數(任何std::string構造函數能接受的參數)之間不匹配纔會被發現。最後的錯誤提示很可能是扭曲的、“感人的”。我使用的一個編譯器報了超過160行的錯誤。

在這個例子中,universal引用只被轉發了一次(從Person的構造函數到std::string的構造函數),但是在更復雜的系統中,universal引用在到達最終決定參數類型是否可接受時,很可能已經轉發好幾次了。universal引用轉發的次數越多,當發生錯誤時,錯誤提示就會越令人困惑。很多開發者發現這個問題就能做爲足夠的理由,讓我們平時不去使用以universal引用爲參數的接口,只有當效率是第一重視點時纔去用它。

在Person的例子中,我們知道轉發函數的universal引用參數應該是一個std::string的初始化列表,所以我們能使用static_assert來確認它是否符合要求。std::is_constructible的type trait能在編譯期判斷一個類型的對象能否由另外不同類型(或者類型集合)的一個對象(或者對象集合)構造出來,所以斷言很同意寫:

class Person {
public:
    template<
      typename T,
      typename = std::enable_if_t<
        !std::is_base_of<Person, std::decay_t<T>>::value
        &&
        !std::is_integral<std::remove_reference_t<T>>::value
      >
    >
    explicit Person(T&& n)      
    : name(std::foward<T>(n))   
    { ... }


    explicit Person(int idx)    
    : name(nameFromIdx(idx))
    { 
        // 斷言std::string能否被T對象創建
        static_assert(
            std::is_constructible<std::string, T>::value,
            "Parameter n can't be used to construct a std::string"
        );
        ...                         // 普通的構造函數在這
    }


    ...                             //  剩下的Person構造函數(和之前一樣)

};

這樣做之後,如果客戶試着用一個不能構造std::string的參數來創建Person,錯誤消息會是確定的。不幸的是,在這個例子中,static_assert是構造函數的一部分,但是轉發代碼,是成員初始化列表的一部分(也就是轉發先於斷言)。在我使用的編譯器中,只有當不尋常的錯誤提示(超過160行)出現之後,我們的static_assert產生的漂亮可讀的錯誤提示纔會出現。

            你要記住的事
  • 將universal引用和重載結合起來的替代品有:用不同的函數名字,pass by lvalue-reference-to-const, pass by value,使用標籤轉發。
  • 通過std::enable_if來限制模板可以讓universal引用和重載一起工作,但是只有在編譯器能使用universal引用重載的時候才能控制條件。
  • universal引用參數常常能帶來效率上的提升,但是它們常常在可用性上有缺陷。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章