C++ 學習筆記之(16)-模板與泛型編程

C++ 學習筆記之(16)-模板與泛型編程

面向對象編程(OOP)和泛型編程都能處理在編寫程序時不知道類型的情況。不同之處在於OOP能處理類型在程序運行之前都未知的情況;而在泛型編程彙總,編譯時即可獲知類型。

定義模板

函數模板

函數模板就是公式,可用來生成針對特定類型的函數版本。

  • template \
template <typename T>
int compare(const T &v1, const T &v2)
{
    if (v1 < v2) return -1;
    if (v2 < v1) return 1;
    return 0;
}

compare(1, 0);  // T 爲 int, 實例化出 int compare(const int&, const int&)
template<typename T, class U> clac(const T&, const U&);
  • 非類型模板參數表示值,而非類型。可用特定的類型名來指定。非類型參數的模板實參必須是常量表達式

    // 比較不同長度的字符串字面值
    template<unsigned N, unsigned M>
    int compare(const char (&p1)[N], const char (&p2)[M])
    {
      return strcmp(p1, p2);
    }
    // 實例化出int compare(const char (&p1)[3], cosnt char (&p2)[4]),編譯器會在字符串字面值常量末尾插入一個空字符
    compare("hi", "hello");  
  • 函數模板可聲明爲inlineconstexpr的,說明符放在模板參數列表之後,返回類型之前

    template<typename T> inline T min(const T&, const T&);
  • 函數模板和類模板成員函數的定義通常放在頭文件中,且模板直到實例化時纔會生成代碼

類模板

類模板是用來生成類的藍圖的。與函數模板不同,編譯器不能爲類模板推斷參數類型

template <typename T> class Blob{ /* ... */ };
Bolb<int> ia = {0}; // 保存 int 的 Blob,實例化出 template <> class Blob<int> { /* ... */ };
  • 類模板的成員函數在類外定義時,須以template開始,後接類模板參數列表

    ret-type StrBlob::member-name(param-list)  // 成員函數類外定義時
    template <typename T>
    ret-value Blob<T>::member-name(param-list)  // 類模板成員函數類模板外定義時
  • 默認情況下,對一個實例化了的類模板,其成員只有在使用時才被實例化

  • 在類模板作用域內,可以直接使用模板名而不提供模板實參。而類模板外定義成員時,必須記住,遇到類名才表示進入類的作用域。

    template <typename T> class BlobPtr{
    public:
        BlobPtr& operator++();  // 編譯器處理時相當於處理 BlobPtr<T>& operator++();
    };
    template <typename T>
    BlobPtr<T> BlobPtr::operator--();  // 返回類型在作用域外,故需要指定模板參數
  • 類與友元各自是否是模板相互無關。若一個類模板包含一個非模板友元,則友元可以訪問所有模板實例。若友元自身是模板,類可以授權所有友元模板實例,也可只授權給特定實例

  • 新標準可爲類模板定義類型別名

    template<typename T> using twin = pair<T, T>;
    twin<string> authors;  // authors 是一個 pair<string, string>
  • 類模板可聲明static成員, 所有實例化的類都共享相同的static成員。要分清模板類、類以及類的對象

模板參數

  • 模板參數會隱藏外層作用域中聲明的相同名字

  • 模板內不能重用模板參數名

    typdef double A;
    template <typename A, typename B> void f(A a, B b)
    {
      A tmp = a;  // tmp 的類型爲模板參數 A 的類型,而非 double
        double B;  // 錯誤:重聲明模板參數 B
    }
  • 模板函數聲明必須包含模板參數,且名字不必與定義中相同

  • 由於作用域運算符::可用來訪問static成員和類型成員。故對於模板代碼來說,無法確定訪問的是名字還是類型。默認情況下,C++語言假定通過作用域運算符訪問的名字而不是類型。可通過使用關鍵字typename顯示告訴編譯器改名字爲一個類型

    // 返回 T 的 value_type 成員
    template <typename T>
    typename T::value_type top(const T& c)
    {
      if (!c.empty())
            return c.back();
        else:
            return typename T::valye_type();  // 若`C`爲空,則返回一個值初始化的元素
    }
  • 默認模板實參(default template argument):新標準可以爲函數和類模板提供默認實參

    // compare 有一個默認模板實參 less<T> 和一個默認函數實參 F()
    template <typename T, typename F = less<T>>
    int compare(const T &v1, const T &v2, F f = F())
    {
      if(f(v1, v2)) return -1;
        if(f(v2, v1)) return 1;
        return 0;
    }
  • 若類模板爲其所有模板參數都提供了默認實參,則若希望使用默認實參,則必須在模板名之後跟一個空尖括號

    template <class T = int> class Numbers{ /* ... */ }  // T 默認爲 int
    Numbers<> ap;  // 空 <> 表示希望使用默認類型
    NUmbers<long double> ldp;

成員模板

一個類(無論是普通類還是類模板)可以包含本身是模板的成員函數,這種成員被稱爲 成員模板(member function, 成員模板不能使虛函數

class DebugDelete{
public:
    DebugDelete(std::ostream &s = std::cerr): os(s) {}
    template <typename T> void operator()(T *p) const
        { os << "Deleteing unique_ptr" << std::endl; delete p;}
private:
    std::ostream &os;
};
double *p = new double;
DebugDelete d;  // 可想 delete 表達式一樣使用的對象
d(p);  // 調用 DebugDelete::operator()(double *), 釋放 p
  • 類模板與其成員模板有各自獨立的模板參數,但類模板外定義成員模板時,類模板的參數列表在前

    template <typename T> class Blob{
      template <typename It> Blob(It b, It e);
        // ...
    }
    // 類模板外定義
    template <typename T> 
    template <typename It> Blob<T>::Blob(It, It e) { /* ... */ }

控制實例化

當模板被使用時纔會進行實例化的特性會導致問題,即多個文件中實例化相同的模板產生嚴重的額外開銷。新標註可通過 顯示實例化(explicit instantiation)解決

  • 使用extern聲明實例,且其中模板參數已被替換爲模板實參, extern表示程序其他位置已有該實例的定義
  • 一個類模板的實例化會實例化該模板的所有成員,包括內聯的成員函數

效率與靈活性

  • shared_ptr在運行時綁定刪除器, 是用戶重載刪除器更爲方便
  • unique_ptr在編譯時綁定刪除器, 避免了間接調用刪除器的u運行時開銷

模板實參推斷

從函數實參來確定模板實參的過程被稱爲 模板實參推斷(template argument deduction)

類型轉換與模板類型參數

  • 將實參傳遞給帶模板類型的函數形參時,能夠自動應用的類型轉換隻有以下兩種

    • const轉換:可以將一個非const對象的引用(或指針)傳遞給一個const引用(或指針)形參
    • 數組或函數指針轉換:如果函數形參不是引用類型,則可以對數組或函數類型的實參應用正常的指針轉換。一個數組實參可以轉換爲一個指向其首元素的指針,函數實參緩緩爲指向該函數類型的指針
    template <typename T> T fobj(T, T);  // 實參被拷貝
    template <typename T> T fref(const T&, const T&);  // 引用
    string s1("a value");
    const string s2("another value");
    fobj(s1, s2);  // 調用 fobj(string, string); const 被忽略
    fref(s1, s2);  // 調用 fref(const string&, const string*), 將 s1 轉換爲 const 是允許的
    int a[10], b[42];
    fobj(a, b);  // 調用 f(int *, int *);
    fref(a, b);  // 錯誤:數組類型不匹配
  • 如果函數參數類型不是模板參數,則對實參進行正常的類型轉換

    template <typename T> ostream &print(ostream &os, const T &obj) { return os << obj; }
    ofstream f("output");
    print(f, 10);  // 使用 print(ostream&, int); 將 f 轉換爲 ostream&

函數模板顯示實參

某些情況下,編譯器無法推斷模板實參的類型,或希望用戶控制模板實例化。當函數返回類型與參數列表中任何類型都不相同時,上述兩種情況經常出現。

// 編譯器無法推斷T1, 它未出現在函數參數列表中
template <typename T1, typename T2, typename T3> T1 sum(T2, T3);  
// 糟糕的設計:用戶必須指定所有三個模板參數
template <typename T1, typename T2, typename T3> T3 alternative_sum(T2, T1);

//T1顯示指定,T2 T3 由函數參數類型推斷而來
auto val3 = sum<long long>(i, lng);  // long long sum(int, long), 
  • 對於模板類型參數顯示指定了的函數實參,可進行正常的類型轉換

    template <typename T> int compare(const T &v1, const T &v2);
    long lng;
    compare<long>(lng, 1024);  //  正確:實例化 compare(long, long);

尾置返回類型與類型轉換

爲了獲得元素類型,可使用標準庫的 類型轉換(type transformation)模板,定義在type_traits

standard_type_transformation_template

// 尾置返回允許我們在參數列表之後表明返回類型,傳入序列,返回序列元素引用
template <typename It> auto fcn(It beg, It end)->decltype(*beg) 
{ 
    return *beg; // 返回序列中一個元素的引用
}
// 使用 typename 告知編譯器 type 表示一個類型,傳入 序列,返回序列元素類型
template <typename It>
auto fcn2(It beg, It end)->typename remove_reference<decltype (*beg)>::type
{
    return *beg;  // 返回序列中一個元素的拷貝
}

函數指針和實參推斷

當函數參數是一個函數模板實例的地址時,程序上下文必須滿足:對每個模板參數,能唯一確定其類型或值

template <typename T> int compare(const T&, const T&);
// pf1 指向實例 int compare(const int&, const int&), T 的模板實參類型爲 int
int (*pf1)(const int &, const int &) = compare;
// func 的重載版本;每個版本接受一個不同的函數指針類型
void func(int (*)(const string&, const string&));
void func(int (*)(const int&, const int&));
func(compare);  // 錯誤:使用 compare 的哪個實例?
func(compare<int>);  // 正確:顯示指定實例化版本 compare(const int&, const int&)

模板實參推斷和引用

從函數調用中推斷類型有兩個規則

  • 編譯器會應用正常的引用板頂規則
  • const是底層的,不是頂層的

從左值引用函數參數推斷類型

  • 當函數參數是模板類型參數的一個普通(左值)引用時(即,形如T&),只能傳遞給它一個左值,若實參是const,則T被推斷爲const類型

    template <typename T> void f1(T&);  // 實參必須是一個左值
    // 對 f1 的調用使用實參所引用的類型作爲模板參數類型
    f1(i);  // i 是一個 int, 模板參數類型 T 是 int
    f1(5);  // 錯誤:傳遞給一個 & 參數的實參必須是一個左值
  • 若函數參數類型爲const T&, 可傳遞任何類型的實參,const爲函數參數類型的一部分,故函數實參的const屬性被忽略,且const不是模板參數類型的一部分

    template <typename T> void f2(const T&);  // 可以接受一個右值
    // f2 中的參數是 const &, 實參中的 const 是無關的。下列調用,函數參數都被推斷爲 const int&
    f2(i);  // i 是一個 int; 模板參數 T 爲 int
    f2(ci);  // ci 是一個 const int, 但模板參數 T 爲 int
    f2(5);  // 一個 const & 參數可以綁定到一個右值, T 爲 int

從右值引用函數參數推斷類型

當函數參數爲右值引用時,可傳遞右值。T 的類型爲右值實參類型

template <typename T> void f3(T&&);
f3(42);  // 實參爲 int 型右值,模板參數 T 爲 int

引用摺疊和右值引用參數

  • 若傳遞左值給函數的右值引用參數,且右值引用指向模板類型參數(如T&&)時,編譯器推斷模板類型參數爲實參的左值引用類型(T&)

  • 引用的引用:被摺疊成普通引用

    • X& &X& &&X&& &都摺疊成類型X&
    • 類型X&& &&摺疊成X&&
  • 引用摺疊只能應用於間接創建的引用的引用,如類型別名或模板參數

    f3(i);  // 實參是一個左值; 模板參數 T 是 int &
    f3(ci);  // 實參是一個最值; 模板參數 T 是一個 const int&
  • 右值引用常用於模板轉發實參或模板重載

理解 std::move

標準庫中move函數的定義

template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
    return static_cast<typename remove_reference<T>::type&&>(t);
}

string s1("hi"), s2;
s2 = std::move(string("bye"));  // 正確:實例化 string&& move(string &&t)
s2 = std::move(s1);  // 正確:實例化 string&& move(string &t)
  • 函數參數T&&爲指向模板類型參數的右值引用,通過引用摺疊,可匹配任何類型實參
  • 通過static_cast顯示地將左值轉換爲右值引用

轉發

某些函數需要將一個或多個實參連同類型不變地轉發給其它函數,在此情況下,需要保持被轉發實參的所有性質,包括實參類型是否爲const以及實參左值右值屬性

  • 通過將函數參數定義爲指向模板類型參數的右值引用,即可保持對應實參的所有類型信息
  • 通過引用參數保持const屬性,因爲引用中const是底層的
  • 若函數參數定義爲T1 &&T2&&, 通過引用摺疊,可保持實參的左值右值屬性
  • 當用於一個指向模板參數類型的右值引用函數參數(T&&)時,forward會保持實參類型的所有細節
template <typename F, typename T1, typename T2>
void flip(F f, T1 &&t1, T2 &&t2) 
{
    f(std::forward<T2>(t2), std::forward<T1>(t1));
}

重載與模板

如果有多個函數提供同樣好的匹配

  • 如果同樣好的函數中只有一個是非模板函數,則選擇此函數
  • 如果同樣好的函數中沒有非模板函數,而有多個函數模板,且其中一個模板比其他模板更特例化,選擇此模板
  • 否則,此調用有歧義

可變參數模板

可變參數模板(variadic template)即一個接受可變數目參數的模板函數或模板類,可變數目的參數被稱爲 參數包(parameter packet), 存在兩種參數包

  • 模板參數包(template parameter packet):表示零個或多個模板參數
  • 函數參數包(function parameter packet):表示零個或多個函數參數
// Args 是一個模板參數包,rest 是一個函數參數包, Args/rest 表示零個或多個模板類型參數/函數參數
template <typename T, typename... Args> void foo(const T &t, const Args& ... rest)
{
    cout << sizeof...(Args) << endl;  // 類型參數的數目
    cout << sizeof...(rest) << endl;  // 函數參數的數目
}
int i = 0; double d = 3.14; string s = "how now brown cow";
// 包中有三個參數,實例化出void foo(cosnt int&, const string&, const int &, const double &);
foor(i, s, 42, d);  

編寫可變參數函數模板

當定義可變參數版本的print時,非可變參數版本的聲明必須在作用域中。否則,可變參數版本會無限遞歸

template <typename T> ostream &print(ostream &os, const T &t)
{
    return os << t;  // 包中最後一個元素之後不打印分隔符
}

template <typename T, typename... Args> 
ostream &print(ostream &os, const T &t, const Args... rest)
{
    os << t << ", ";
    return print(os, rest...);
};
print(cout, i, s, 42);  // 包中有兩個參數,輸出 0, how now brown cow, 42

包擴展

對參數包,還可擴展(expand), 擴展包時,需提供用於每個擴展元素的模式(pattern)。擴展包即將包分解爲構成的元素,對每個元素應用模式,獲得擴展後的列表,通過在模式右邊添加省略號...觸發擴展操作

template <typename... Args> ostream &errorMsg(ostream &os, cosnt Args&... rest)
{
    // print(os, debug_rep(al), debug_rep(a2), ..., debug_rep(an))
    return print(os, debug_rep(rest)...);
}
errorMsg(cerr, fcnName, code.num(), otherData, "other", item); // 等價於下式
print(cerr, debug_rep(fcnName), debug_rep(code.num()), debug_rep(otherData),
     debug_rep("otherData"), debug_rep(item));
// 將包傳遞給debug_rep; print(os, debug_rep(a1, a2, ..., an))
print(os, debug_rep(rest...));  // 錯誤:此調用無匹配函數
  • print調用使用了模式debug_rep(rest), 表示對函數參數包rest中每個元素調用debug_rep
  • 擴展中的模式會獨立地應用於包中的每個元素

轉發參數包

在新標準下,可組合使用可變參數模板與forward機制來編寫函數,實現將實參不變地傳遞給其它函數

template <typename... Args> void fun(Args&&... args)  // 將 Args 擴展爲一個右值引用的列表
{
    // work 的實參即擴展 Args 又擴展 args
    work(std::forward<Args>(args)...);
}

模板特例化

通用模板的定義可能不適合特定類型,故此時可定義類或函數模板的一個特例化版本

// 第一個版本:可以比較任意兩個類型
template <typename T> int compare(const T&, const T&);
// 第二個版本處理字符串字面常量,處理的是數組,不是指針,指針無法轉換爲數組引用
template <size_t N, size_t M> int compare(const char (&)[N], const char (&)[M]);
// compare 的特例化版本,處理字符數組的指針
template <> int compare(const char *const &p1, const char *const &p2);

const char *p1 = "hi", *p2 = "mom";
compare(p1, p2);  // 調用第一個模板
compare("hi", "mom");  // 調用有兩個非類型參數的版本,若存在第三個函數,即模板特例化函數,選用函數3
  • 特例化的本質是實例化一個模板,而非重載它。故,特例化不影響函數匹配
  • 模板及其特例化版本應該聲明在同一個頭文件中,所有同名模板的聲明應該放在前面,然後是這些模板的特例化版本

結語

  • 模板是C++語言與衆不同的特性,也是標準庫的基礎。一個模板就是一個編譯器用來生成特定類型或函數的藍圖。生成特定類或函數的過程稱爲實例化。我們只編寫一次模板,就可以將其用於多種類型和值,編譯器會爲每種類型和值進行模板實例化。
  • 我們既可以定義函數模板,也可以定義類模板。標準庫算法都是函數模板,標準庫容器都是類模板
  • 顯示模板實參允許我們固定一個或多個模板參數的類型或值。對於指定了顯示模板實參的模板參數,可以應用正常的類型轉換
  • 一個模板特例化就是一個用戶提供的模板實例,它將一個或多個模板參數綁定到特定類型或值上。當我們不能(或不希望)將模板定義用於某些特定類型時,特例化非常有用
  • 最新C++標準的一個主要部分是可變參數模板。一個可變參數模板可以接受數目和類型可變的參數。可變參數模板允許我們編寫像容器的emplace成員和標準庫make_shared函數這樣的函數,實現將實參傳遞給對象的構造函數
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章