第16課 處理萬能引用和重載的關係

一.萬能引用形參重載函數的問題ios

(一)當產生精確匹配: C++的重載匹配是貪婪的,當形參爲萬能引用類型時,實例化過程當中,它和幾乎任何的實參類型都會產生精確匹配。編程

  1. 根據重載匹配規則,精確匹配優先於類型轉換的函數。一旦萬能引用成爲重載候選函數,就會吸引發大批的實參類型。所以,形參爲萬能引用的重載函數在匹配時會被調用。數據結構

  2. 萬能引用類型的重載函數在完美轉發構造函數中的問題更爲嚴重。由於對於很是量的左值類型而言,它通常會造成比複製構造函數更精確的匹配,並且還會劫持派生類中對基類的複製和移動構造函數的調用。函數

 (二)當產生相等匹配:若在函數調用時,一個模板函數和一個普通函數(非模板類型的函數)具有相等的匹配程度,則優先選用普通函數性能

【編程實驗】完美轉發與重載函數的衝突spa

#include <iostream>
#include <set>     //for multiset
#include <chrono>  //for std::chrono::system_clock::now()
 
using namespace std;

using timepoint_t = std::chrono::system_clock::time_point;
std::multiset<std::string> names;  //全局數據結構

void log(timepoint_t now, const string& content){}
string nameFromIdx(int idx) { return "abc";}

//1.普通函數與完美轉發函數構成的重載關係
//1.1 形參爲string
void logAndAdd(const std::string& name)
{
    auto now = std::chrono::system_clock::now(); //取得當前時間
    log(now, "logAndAdd");
    names.emplace(name);
    cout << "void logAndAdd(const std::string& name)" << endl;
}
//1.2 形參爲int:經過索引查找名字,並記錄到names中
void logAndAdd(int idx)
{
    auto now = std::chrono::system_clock::now(); //取得當前時間
    log(now, "logAndAdd");
    names.emplace(nameFromIdx(idx));
    cout << "void logAndAdd(int idx)" << endl;
}

//1.3 形參爲萬能引用類型(即構成完美轉發)
template<typename T>
void logAndAdd(T&& name)
{
    auto now = std::chrono::system_clock::now(); //取得當前時間
    log(now, "logAndAdd");
    names.emplace(std::forward<T>(name));
    cout << "void logAndAdd(T&& name)" << endl;
}

//2. 構造函數與完美轉發函數構成的重載關係
class Person
{
    std::string name;
public:
    template<typename T>
    explicit Person(T&& n):name(std::forward<T>(n)) //完美轉發構造函數
    {
        cout << "explicit Person(T&& n):name(std::forward<T>(n))"<< endl;
    } 

    explicit Person(int idx) : name(std::move(nameFromIdx(idx))){}  //形參爲int的構造函數
    
    /*如下兩個特殊成員函數是編譯器自動生成的,爲了便於觀察,羅列出來
    Person(const Person& rhs);  //複製構造函數(編譯器自動生成)
    Person(Person&& rhs);       //移動構造函數(編譯器自動生成)
    */
};

class SpecialPerson : public Person
{
public:
    using Person::Person; //繼承構造函數

    /*複製構造函數,調用的是基類的完美轉發函數!
    error, 由於rhs的類型爲SpecialPerson,當調用Person(rhs)時Person類的模板函數
    會產生一個比默認的構造函數更精確的匹配函數Person(SpecialPerson& n),但name的構造函數中並無SpecialPerson的重載版本。*/
    //SpecialPerson(const SpecialPerson& rhs): Person(rhs){}

    /*移動構造函數,調用的是基類的完美轉發函數,而非默認的移動構造!
    error, 緣由同上*/
    //SpecialPerson(SpecialPerson&& rhs) : Person(std::move(rhs)){}
};

int main()
{
    //1. 普通函數與完美轉發構成的重載關係
    //1.1 調用普通函數時:void logAndAdd(const std::string& name)
    std::string petName("Darla");
    logAndAdd(petName);                   //傳遞左值。因爲petName是左值,會被複制到names中且沒法避免!(一次構造)
    logAndAdd(std::string("Persephone")); //傳遞右值。建立string臨時對象,因爲name自己是左值,會並被複制到names中。(一次構造和一次複製)
    logAndAdd("Patty Dog");               //傳遞字符串字面量。先建立string臨時對象,並被複制到names (一次構造和一次複製)

    //1.2 調用模板函數時:void logAndAdd(T&& name)
    logAndAdd(petName);                   //傳遞左值。一如此前
    logAndAdd(std::string("Persephone")); //傳遞右值。建立string臨時對象,會並被移動到names中。(一次構造和一次移動)
    logAndAdd("Patty Dog");               //傳遞字符串字面量。將const char[10]&傳遞給names,(在multiset中直接構造,僅一次構造!!!)

    logAndAdd(22); //調用void logAndAdd(int idx)

    short nameIdx = 0;
    //logAndAdd(nameIdx); //編譯失敗,由於形參爲short,此時函數模板會產生比logAndAdd(int idx)更精確的匹配函數:void logAndAdd(short idx)
                          //從而,轉去調用模板實例化後的void logAndAdd(short idx)函數,當模板中調用names.emplace(std::forward<T>(name))時
                          //會將nameIdx這個short類型的實參傳給names中的emplace函數,但其並無形參爲short類型的重載函數,所以報錯。(注意,
                          //儘管nameIdx能夠經過類型提高轉化爲int類型,從而匹配logAndAdd(int idx)函數,但根據C++匹配的貪婪性,精確匹配優先於
                          //類型提長後的匹配函數。

    //2. 構造函數與完美轉發構成的重載關係
    Person p("Nancy");
    //auto cloneOfP(p); //error,至關於Person cloneOfP(p);本意要調用默認的複製構造函數,但因爲p是非const左值,此時模板會生成更精確匹配的
                        //Person(Person& n):name(std::forward<Person&>(n))構造函數。這裏會將Person對象傳入string的構造函數,所以報錯。
    const Person p2("Nancy");
    auto cloneOfP(p2);  //ok,調用默認的複製構造函數Person(const Person& p);雖然此時模板函數也會實例化出一個與複製構造函數簽名相同的函數。
                        //但根據C++重載匹配規則,當具備相等的匹配程度時,普通函數優先於模板函數調用。

    return 0;
}

二. 替代方案code

(一)放棄重載對象

  1. 方法:重命名函數名稱,放棄重載。如將logAndAdd函數改成logAndAddName和logAndAddNameIdx兩個版本的函數。blog

  2. 不足:不適用於構造函數處理,由於構造函數的名稱是由語言固化的。繼承

(二)傳遞const T&類型的形參

  1. 方法:使用傳遞左值常量類型來代替萬能引用類型。如將logAndAdd(T&& name)替換爲logAndAdd(const string& n)。

  2. 不足:達不到使用萬能引用類型的高效率。

(三)傳值

  1. 方法:將按引用傳遞改成按值傳遞參數。(如將Person(T&& n)構造函數改成Person(string n)

  2. 注意事項:只有在確定會發生複製形參時,才考慮使用按值傳遞。

(四)標籤分派

  1. 方法:函數形參中除了萬能引用類型外,再設置一個非萬能引用的形參做爲「標籤」利用該標籤的差別進行函數分派。如本例中logAndAdd是向外提供的接口函數,這是一個「單版本」(無重載版本)的函數,它把待完成的工做分派到重載的實現函數(logAndAddImpl),logAndAddImpl利用了第2個形參充分的差別性進行分派,其中的 std::true_type和std::false_type就是所謂的「標籤」。

  2. 特色:

    ①重載函數接受一個萬能引用形參,還有一個「標籤」。在重載匹配時,不只對這個萬能引用類型有依賴,還對標籤有依賴。標籤值才決定了調用哪一個重載版本

    ② 「放棄重載」、「傳遞const T&類型的形參」、「傳值」等3種方法都放棄使用萬能引用類型,但標籤分派既可以使用萬能引用,又沒有放棄重載。

  3. 不足:

    ①構造函數比較特殊,若是隻編寫一個構造函數,而後在其中運用標籤分派,那麼有些針對構造函數調用就可能會被默認的構造函數接手處理,從而繞過標籤分派系統。

    ②複製很是量左值時,總會調用萬能引用類型構造函數。若是基類只聲明一個完美轉發構造函數,派生類以傳統方式實現複製和移動構造函數時,總會調用到基類的完美轉發構造函數,但正確的行爲應該是調用到基類的複製或移動構造函數。

(五)對接受萬能引用的模板施加限制

  1. 方法:利用std::enable_if,只有知足其指定的條件時才啓用該函數模板。

  2. 特色:利用完美轉發,效率高。還能夠控制萬能引用和重載的組合,而非簡單地禁用之。該技術能夠應用於重載沒法避免的場合(如構造函數)。

三. 方案的權衡

(一)前三種技術(「放棄重載」、「傳遞const T&類型的形參」、「傳值」)都須要對待調用的函數形參逐一指定類型。而後兩種技術(標籤分派和模板限制)則利用了完美轉發,所以無須指定形參類型

(二)完美轉發效率更高。但也有不足:

  1. 針對某些類型沒法實施完美轉發(如大括號初始化列表、0或NULL等)

  2. 萬能引用被轉發的次數越多,出錯時的錯誤信息就越多,甚至很難理解。

  3. 萬能引用形參一般在性能方面具備優點,但易用性方面通常有劣勢。

(三)利用std::enable_if對模板施加限制,能夠將萬能引用和重載一塊兒使用。但只有在編譯器能使用萬能引用重載的地方才能起做用。

(四)應避免依萬能引用類型進行重載

【編程實驗】替代方案

#include <iostream>
#include <set>     //for multiset
#include <chrono>  //for std::chrono::system_clock::now()
 
using namespace std;

using timepoint_t = std::chrono::system_clock::time_point;
std::multiset<std::string> names;  //全局數據結構

void log(timepoint_t now, const string& content){}
string nameFromIdx(int idx) { return "abc";}

//1. 普通函數與完美轉發構成的重載(解決方案:標籤分派)
//1.1 前向聲明
template<typename T>
void logAndAdd(T&& name); 

//1.2 重載的實現函數
template<typename T>
void logAndAddImpl(T&& name, std::false_type) //將第2個形參做爲標籤,利用其差別性實施分派
{
    auto now = std::chrono::system_clock::now(); //取得當前時間
    log(now, "logAndAdd");
    names.emplace(std::forward<T>(name));
    cout << "void logAndAddImpl(T&& name, std::false_type)" << endl;
}

//1.3 重載的實現函數
template<typename T = int>
void logAndAddImpl(int idx, std::true_type) //將第2個形參做爲標籤,利用其差別性實施分派
{
    logAndAdd(nameFromIdx(idx));
    cout << "void logAndAddImpl(int idx, std::true_type)" << endl;
}

//1.4 根據T的類型進行分派
template<typename T>
void logAndAdd(T&& name) //函數的聲明仍然保持不變
{
    //分派的目標函數
    logAndAddImpl(std::forward<T>(name), std::is_integral<typename std::remove_reference<T>::type>());
}


//2. 構造函數與完美轉發函數構成的重載(解決方案:使用傳值或enable_if)
class Person
{
    std::string name;
public:
    //第三種方法:傳值
    //explicit Person(std::string n) :name(std::move(n)) //替換掉完美轉發T&&類型的構造函數
    //{
    //    cout << "explicit Person(std::string n):name(std::move(n))" << endl;
    //}

    //第五種方法: 使用std::enable_if來啓用函數模板
    //僅當T不是Person類型時才啓用該構造函數。(如,在複製對象時避開該函數,轉而去調用Person(const Person&))
    //template<typename T, typename = typename std::enable_if< //decay能夠去除T的引用和cv修飾符。
    //                                                        !std::is_same<Person, typename std::decay<T>::type>::value>
    //                                                        ::type>
    //explicit Person(T && n) { cout << "Person(T&& n): T != Person" << endl; }

    //接受std::string類型以及能夠轉化爲std::string類型實參類型的構造函數。
    template<typename T, typename = std::enable_if_t<
                                       !std::is_base_of<Person, std::decay_t<T>>::value  //T爲非Person及其子類
                                       && 
                                       !std::is_integral<std::remove_reference_t<T>>::value> > //T爲非整型
    explicit Person(T&& n):name(std::forward<T>(n)){ cout << "Person(T&& n)" << endl; }

    //接受實參爲整型的構造函數
    explicit Person(int idx) : name(std::move(nameFromIdx(idx))) { cout << "Person(int idx)" << endl; }   //同前一個例子
    
    /*如下兩個特殊成員函數是編譯器自動生成的,爲了便於觀察,羅列出來
    Person(const Person& rhs);  //複製構造函數(編譯器自動生成)
    Person(Person&& rhs);       //移動構造函數(編譯器自動生成)
    */
};

class SpecialPerson : public Person
{
public:
    using Person::Person; //繼承構造函數

    /*複製構造函數:基類的完美轉發函數被限制,會轉而調用咱們想要的默認構造函數!*/
    SpecialPerson(const SpecialPerson& rhs): Person(rhs){} //以傳統的方式實現複製構造

    /*移動構造函數:基類的完美轉發函數被限制,會轉而調用咱們想要的默認構造函數*/
    SpecialPerson(SpecialPerson&& rhs) noexcept : Person(std::move(rhs)){}//以傳統的方式實現移動構造
};

int main()
{
    //1. 普通函數與完美轉發構成的重載關係
    std::string petName("Darla");
    logAndAdd(petName);                   //傳遞左值。因爲petName是左值,會被複制到names中且沒法避免!(一次構造)
    logAndAdd(std::string("Persephone")); //傳遞右值。建立string臨時對象,因爲name自己是左值,會並被複制到names中。(一次構造和一次複製)
    logAndAdd("Patty Dog");               //傳遞字符串字面量。先建立string臨時對象,並被複制到names (一次構造和一次複製)
    logAndAdd(20);

    //2. 構造函數與完美轉發構成的重載關係
    Person p("Nancy");
    auto cloneOfP(p); //ok,至關於Person cloneOfP(p);調用默認構造函數

    SpecialPerson sp("SantaClaus");
    SpecialPerson sp2(sp); //ok,調用基類默認構造函數
    SpecialPerson sp3(10); //ok,調用基類Person(int idx)

    return 0;
}
相關文章
相關標籤/搜索