1. 程式人生 > 其它 >C++ Primer學習筆記 - 第16章 模板與泛型程式設計(二)

C++ Primer學習筆記 - 第16章 模板與泛型程式設計(二)

目錄

上半部分,見C++ Primer學習筆記 - 第16章 模板與泛型程式設計(一)

16.3 過載與模板

函式模板可以被另一個模板或普通非模板函式過載。跟普通函式過載一樣,名字相同的函式必須具有不同數量或型別的引數。

如果涉及到函式模板,則函式匹配規則會在下面幾個方面受到影響:

  • 對於一個呼叫,其候選函式包括所有模板實參推斷成功的函式模板例項。
  • 候選的函式模板總是可行的,因為模板實參推斷會排除任何不可行的模板。
  • 與往常一樣,可行函式(模板與非模板)按型別轉換(如果對此呼叫需要的話)來排序。當然,可用於函式模板呼叫的型別轉換非常有限,只有const轉換、陣列或函式指標轉換,見16.2.1
  • 與往常一樣,如果恰有一個函式提供比任何其他函式都更好的匹配,則選擇此函式。但是,如果有多個函式提供同樣好的匹配,則:
    1)如果同樣好的函式中只有一個是非模板函式,則選擇此函式。
    2)如果同樣好的函式中沒有非模板函式,而有多個函式模板,且其中一個模板比其他模板更特例化,則選擇此模板。
    3)否則,則此呼叫有歧義。

編寫過載模板

我們定義2個版本的函式模板debug_rep,用來列印我們不能處理的型別。第一個模板接受一個const物件的引用,第二個模板接受一個指標型別。

// 第一個版本函式模板
template <typename T> string debug_rep(const T& t)
{
    ostringstream ret;
    ret << t;           // 使用T的輸出運算子列印t的一個表示形式
    return ret.str();   // 返回ret繫結的string的一個副本
}

// 第二個版本函式模板
// 注意:此函式不能用於char*
template <typename T> string debug_rep(T* p)
{
    ostringstream ret;
    ret << "pointer: " << p;         // 列印指標本身的值(地址)
    if (p)
        ret << " " << debug_rep(*p); // 列印p指向的值
    else
        ret << " null pointer";      // 或指出p為空
    return ret.str();                // 返回ret繫結的string的一個副本
}

注意:第二個版本雖然接受指標型別,但不能用於列印C風格字串指標,因為IO庫為char*值定義了一個<<版本,該版本假定指標表示一個空字元結尾的字元陣列,並列印陣列內容而非地址值。因此雖然列印C風格字串指標可能不會出錯,但列印的內容並不符合預期。

如何使用上面定義的函式?

string s("hi");
cout << debug_rep(s) << endl; // s非指標,因此從第一個模板例項化函式

cout << debug_rep(&s) << endl; // s為指標,因此從第二個模板例項化函式

debug_rep(&s)也能用第一個版本生成例項,為什麼編譯器會選擇第二個版本?
因為,雖然兩個函式都能生成可行的例項:

  • debug_rep(const string&),由第一個版本的debug_rep例項化而來,T被繫結到string
  • debug_rep(string*),由第二個版本的debug_rep例項化而來,T被繫結到string。

但是,第二個版本的debug_rep例項是此呼叫的精確匹配。第一個版本的例項需要進行普通指標(string)到const指標(const string&)的轉換。正常函式匹配規則告訴我們應當選擇第二個模板。

多個可行模板

另外一個呼叫:

const string* sp = &s;
cout << debug_rep(sp) << endl;

此例中,2個版本的例項都是精確匹配:

  • debug_rep(const string&),由第一個版本的debug_rep例項化而來,T被繫結到string
  • debug_rep(const string*),由第二個版本的debug_rep例項化而來,T被繫結到const string。

此時,正常函式匹配規則無法區分這2個函式,我們可能會決定這個呼叫有二義性。但,根據過載函式模板的特殊規則,此呼叫被解析為debug_rep(T*),即,更特例化的版本。

設計這條規則的原因:沒有它,將無法對一個const指標呼叫指標版本的debug_rep。
模板debug_rep(const T&)本質上可以用於任何型別,包括指標型別。該模板比debug_rep(T*)更通用,後者只能用於指標型別,也就是說後者更加特例化。

PS:當有多個過載模板對一個呼叫提供同樣好的匹配時,應選擇最特例化的版本。

非模板和模板過載

定義一個普通的debug_rep(非模板),來列印雙引號包圍的string:

// 定義普通函式版本的debug_rep 
// 列印雙引號包圍的string
string debug_rep(const string& s)
{
    return '"' + s + '"';
}

此時,同樣有2個可行函式:

  • debug_rep(const string&),第一個模板,T被繫結到string。
  • debug_rep(const string&),普通非模板函式。

此時,雖然2個函式具有相同引數列表,具有同樣好匹配,但編譯器會選擇非模板版本。因為有多個同樣匹配的函式時,編譯器會選擇最特例化的版本,也就是普通非模板函式。

PS:對於一個呼叫,如果一個非函式模板與一個函式模板提供同樣的匹配,則選擇非模板版本。因為前者更特例化。

過載模板和型別轉換

有一種情況還沒討論到:C風格字串指標和字串字面常量。

考慮呼叫:

cout << debug_rep("hi world!") << endl; // 呼叫debug_rep(T*)

本例中,所有三個debug_rep版本都是可行的:

  • debug_rep(const T&),T被繫結到char[10]。
  • debug_rep(T*),T被繫結到const char。
  • debug_rep(const string&),要求從const char*到string的型別轉換。

編譯器會選擇第二個版本例項,因為這個是最特例化的。

然而,第二個版本並不會將字串按string處理,因為IO庫為char*提供了專門的<<,會假設字串以NUL-byte結尾。
如果希望將字串按string處理,可以定義另外兩個非模板過載版本:

// 將C字串指標轉換為string,並呼叫string版本的debug_rep(轉交給另外一個版本的debug_rep處理)
string debug_rep(char* p)
{
  return debug_rep(string(p));
}
string debug_rep(const char* p)
{
  return debug_rep(string(p));
}

缺少宣告可能導致程式行為異常

對於過載函式模板的函式,如果忘記了宣告的函式,編譯器可以從模板例項化出與呼叫匹配的版本,從而導致難以察覺的錯誤。因此,必須保證呼叫的函式模板,在作用域內有對應宣告的函式。
比如,為了使char*版本的debug_rep 正確工作,定義此版本時,debug_rep(const string&)的宣告必須在作用域中;否則,可能呼叫錯誤的debug_rep版本

template <typename T> string debug_rep(const T& t);
template <typename T> string debug_rep(T* p);
// 為使debug_rep(char*)定義正確的構造,下面的宣告必須在作用域中
string debug_rep(const string& s);
string debug_rep(char* p)
{
    return debug_rep(string(p));
}

16.4 可變引數模板

一個可變引數模板(variadic template)是一個接受可變數目引數的模板函式或模板類。可變數目的引數被稱為引數包(parameter packet)。存在兩種引數包:模板引數包(template parameter packet),表示0個或多個模板引數;函式引數包(function parameter packet),表示0個或多個函式引數。

怎麼表示一個模板引數或函式引數的包?
可以用一個省略號來指出一個模板引數或函式引數表示一個包,如class...或typename...,指出接下來的引數表示0個或多個型別的列表;一個型別名後面跟一個省略號表示0個或多個給定型別的非型別引數的列表。

// Args 是一個模板引數包;rest是一個函式引數包
// Args 表示零個或多個模板型別引數
// rest 表示零個或多個函式引數
template <typename T, typename... Args>    // 這裡... 指出Args是一個模板引數包
void foo(const T& t, const Args&... rest); // 這裡... 指出rest是一個函式引數包,Args&是其型別

上面的語句聲明瞭foo是一個可變引數函式模板,有一個名為T的型別引數,和一個名為Args的模板引數包。這個包表示零個或多個額外的型別引數。foo的函式引數列表包含一個const&型別的引數,指向T的型別,還包含一個名為rest的函式引數包,此包表示零個或多個函式引數。

編譯器會從函式的實參推斷模板引數型別。對於一個可變引數模板,編譯器還會推斷包中引數的數目。例如,下面的呼叫:

int i = 0; double d = 3.14; string s = "how now brown cow";
foo(i, s, 42, d);
foo(s, 42, d);
foo(d, s);
foo("hi");

編譯器會為foo例項化出4個不同的版本:

void foo(const int&, const string&, const int&, const double&);
void foo(const string&, const int&, const char[3]&);
void foo(const double&, const string&);
void foo(const char[3]&);

每個例項中,T的型別都是從第一個實參的型別推斷出來的。剩下的實參(如果有的話)提供函式額外實參的數目和型別。

sizeof...運算子

當我們想知道包中有多少元素時,該怎麼辦?
可以使用sizeof...運算子。類似於sizeof,sizeof...運算子也返回一個常量表達式,而且不會對實參求值:

比如,可以直接用上面的例子,

// 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; // 函式引數的數目
}

...
foo(i, s, 42, d); // 列印3,3
foo(s, 42, d);    // 列印2,2
foo(d, s);        // 列印1,1
foo("hi");        // 列印0,0

PS:sizeof...求引數包中引數個數時,只能在模板內或函式內使用。

16.4.1 編寫可變引數函式模板

我們知道initializer_list可定義一個可接受可變數目實參的函式,但是initializer_list有其侷限:所有實參必須具有相同型別(或它們的型別可以轉換為一個公共型別)。
當我們既不知道要處理的實引數目,也不知道其型別時,可變參函式就很有用。而可變參函式模板在這方面,就比較有效。

我們定義一個print函式,它在一個給定流上列印給定實參列表的內容。
可變引數函式通常是遞迴的。第一步,呼叫處理包中的第一個實參,然後用剩餘實參呼叫自身。print函式也是這樣的模式,每次遞迴呼叫將第二個實參列印到第一個實參表示的流中。為終止遞迴,我們還需要定義一個非可變引數的print函式,它接受一個流和一個物件:

// 用來終止遞歸併列印最後一個元素的函式
// 此函式必須在可變引數版本的print定義之前宣告
template <typename T>
ostream& print(ostream& os, const T& t)
{
    return os << t;
}

// 包中除了最後一個元素外的其他元素都會呼叫這個版本的print
template <typename T, typename... Args>
ostream& print(ostream& os, const T& t, const Args&... rest)
{
    os << t << ", ";           // 列印第一個實參
    return print(os, rest...); // 遞迴呼叫,列印其他實參
}

// 呼叫示例
print(cout, 2, "hello", "a"); // 列印 2, hello, a

PS:當定義可變引數版本的print時,非可變引數版本的宣告必須在作用域中。否則,可變引數版本會無限遞迴。

16.4.2 包擴充套件

對於一個引數包,除了用sizeof...獲取其大小(實參個數),能對它做的唯一事情就是擴充套件(expand)它。
當擴充套件一個包時,還有提供用於每個擴充套件元素的模式(pattern)。擴充套件一個包,就是將它分解為構成的元素,對每個元素應用模式,獲得擴充套件後的列表。我們通過在模式右邊放一個省略號(...)來觸發擴充套件操作。

例如,自定義print函式包含2個擴充套件:

template <typename T, typename... Args>
ostream& print(ostream& os, const T& t, const Args&... rest)   // 擴充套件Args
{
  os << t << ",";
  return print(os, rest...);                                   // 擴充套件rest
}

第一個擴充套件操作擴充套件模板引數包(Args),為print生成函式引數列表。
第二個擴充套件操作出現在對print的呼叫中。此模式為print呼叫生成實參列表。

在對Args的擴充套件中,編譯器將模式const Args&應用到模板引數包Args中的每個元素。因此,此模式的擴充套件結果是一個逗號分隔的零個或多個型別的列表,每個型別都形如const type&。例如:

print(cout, i, s, 42); // 包中有2個引數

最後2個實參的型別和模式一起確定了尾置引數的型別。此呼叫被例項化為:

ostream& print(ostream&, const int&, const string&, const int&);

理解包擴充套件

前面的print中函式引數包通過遞迴方式,僅僅將包擴充套件為其構成元素,C++還允許更復雜的擴充套件模式。

例如,我們可以編寫第二個可變引數函式,對其每個實參呼叫debug_rep,然後呼叫print列印結果string:

// 在print呼叫中對每個實參呼叫debug_rep
template <typename... Args>
ostream& errorMsg(ostream& os, const Args&... rest)
{
//    print(os, debug_rep(a1), debug_rep(a2), ..., debug_rep(an));
    return print(os, debug_rep(rest)...);
}

該print呼叫使用了模式debug_rep(rest)。此模式表示我們希望對函式引數包rest中的每個元素呼叫debug_rep。擴充套件結果是將一個逗號分隔的debug_rep呼叫列表。
例如,呼叫:

errorMsg(cerr, fcnName, code.num(), otherData, "other", item);

就好像我們這樣編寫程式碼:

print(cerr, debug_rep(fcnName), debug_rep(code.num()),
      debug_rep(otherData), debug_rep("other"),
      debug_rep(item));

相對的,下面的模式會編譯失敗:

// 將包傳遞給debug_rep; print(os, debug_rep(a1, a2, ..., an))
print(os, debug_rep(rest...)); // 錯誤:此呼叫無此匹配函式

這段程式碼問題在哪兒?
問題在於我們在debug_rep呼叫中擴充套件了rest,也就是說,這段程式碼等價於:

print(os, debug_rep(fcnName, code.num(), otherData, "otherData", item)); // 錯誤:debug_rep並沒有定義可變引數版本

顯然,這段等價程式碼是錯誤的,因為debug_rep沒有定義可變引數版本。