函式模版


經常地,你會撰寫相同演算流程的函式,雖然參數型態不同,然而物件的協定相同:

bool greaterThan(int a, int b) {
    return a > b;
}

bool greaterThan(string a, string b) {
    return a > b;
}

這時就會希望,參數的型態也可以…呃…參數化,也就是 ab 的型態可以在呼叫時指定,而不是如上寫死了兩個版本,這時可以使用函式模版:

#include <iostream> 
using namespace std; 

template <typename T>
bool greaterThan(T a, T b) {
    return a > b;
}

int main() { 
    // 顯示 0
    cout << greaterThan(10, 20) << endl;

    // 顯示 1
    cout << greaterThan(string("xyz"), string("abc")) << endl;

    return 0; 
} 

在這個範例中,greaterThan 是個函式模版(function template),或稱為泛型函式(generic function),定義模版時使用 template,之後跟著模版參數列,typename 定義了一個模版參數 T,若有多個模版參數,各自都要使用 typename 來定義,每個模版參數以逗號區隔。

程式碼 greaterThan(10, 20) 建立了一個模版的實例(instance),相當於 greaterThan<int>(10, 20),而 greaterThan(string("xyz"), string("abc")) 建立了另一個模版實例,相當於 greaterThan(string("xyz"), string("abc"))

建立一個模版實例的意思是,編譯器推斷出 T 的型態,產生並編譯了一個對應版本,這就是之所以名為模版的原因,也就是說編譯器以你定義的模版為基礎,為 greaterThan(10, 20) 建立了 bool greaterThan(int, int),為 greaterThan(string("xyz"), string("abc")) 建立了 bool greaterThan(string, string)

如果有某個版本,不想要編譯器建立,而想要自行實作呢?可以明確地定義特化版本:

#include <iostream> 
using namespace std; 

template <typename T>
bool greaterThan(T a, T b) {
    return a > b;
}

template <>
bool greaterThan(string s1, string s2) {
    return s1.size() > s2.size();
}

int main() { 
    cout << greaterThan(10, 20) << endl;
    cout << greaterThan(string("xyz"), string("abc")) << endl;

    return 0; 
} 

在這個例子中,也許你想比的是字串的長度而不是字典順序,為此建立了特化版本,因此編譯器就不會自行建立 bool greaterThan(string, string) 的版本,而是使用你定義的特化版本,執行結果就都顯示 0 了。

現在來看看一個需求,傳遞陣列給函式時會使用指標,一個例子是:

#include <iostream> 
using namespace std; 

void printAll(int *arr, int len) {
    for(int i = 0; i < len; i++) {
        cout << arr[i] << " ";
    }
    cout << endl;
}

int main() { 
    int arr1[] = {1, 2};
    int arr2[] = {3, 4, 5};

    printAll(arr1, 2);
    printAll(arr2, 3);

    return 0; 
} 

如果想在 printAll 中使用 for range 可行嗎?就上例來說沒辦法,根據〈指標與陣列〉與〈指標的指標〉的說明,必須改為以下:

#include <iostream> 
using namespace std; 

void printAll(int (*arr)[2]) {
    for(auto elem : *arr) {
        cout << elem << " ";
    }
    cout << endl;
}

void printAll(int (*arr)[3]) {
    for(auto elem : *arr) {
        cout << elem << " ";
    }
    cout << endl;
}

int main() { 
    int arr1[] = {1, 2};
    int arr2[] = {3, 4, 5};

    printAll(&arr1);
    printAll(&arr2);

    return 0; 
} 

問題是解決了,然而也在參數上寫死了陣列長度,仔細看看兩個函式的實作內容是相同的,這時你會想,編譯器可以為函式模版推斷出對應型態的版本,那可否推斷出陣列長度的值呢?可以喔!

#include <iostream> 
using namespace std; 

template <typename T, int L>
void printAll(T (*arr)[L]) {
   for(auto elem : *arr) {
       cout << elem << " ";
   }
   cout << endl;
}

int main() { 
    int arr1[] = {1, 2};
    int arr2[] = {3, 4, 5};

    printAll(&arr1);
    printAll(&arr2);

    return 0; 
} 

在上例中,L 並不是以 typename 定義,而是 int,這稱為模版的非型態參數(nontype parameter),編譯器會試著為非型態參數推斷出一個值,值的推斷來源必須是個常數運算式,也就是靜態時期可決定的值。

上例可以使用參考,令呼叫時更直覺一些,例如:

#include <iostream> 
using namespace std; 

template <typename T, int L>
void printAll(T (&arr)[L]) {
   for(auto elem : arr) {
       cout << elem << " ";
   }
   cout << endl;
}

int main() { 
    int arr1[] = {1, 2};
    int arr2[] = {3, 4, 5};

    printAll(arr1);
    printAll(arr2);

    return 0; 
} 

實際上,T (&arr)[L] 的宣告是多此一舉,既然都用了模版了,底下照樣也行得通:

#include <iostream> 
using namespace std; 

template <typename T>
void printAll(T &arr) {
   for(auto elem : arr) {
       cout << elem << " ";
   }
   cout << endl;
}

int main() { 
    int arr1[] = {1, 2};
    int arr2[] = {3, 4, 5};

    printAll(arr1);
    printAll(arr2);

    return 0; 
}