Function Templates 前言 定義函式樣板 函式樣板的實例化 Template Argument Deduction Explict Template Arguments Template Compilation Models Template Explicit Specialization Overloading Function Templates
前言 C++ 是一個 strongly typed 的電腦語言,這使得我們必須為用意相同但參數資料型態不同的函式,寫出個別的定義。譬如: int min (int x, int y) { return (x < y)? x : y } double min (double x, double y) { return (x < y)? x : y } 查看上面兩個 min 函式,你不難發現到它們的程式碼除了傳回值和參數的資料型態有所不同以外,其它都一樣。 基於以上的觀察,C++ 提供資料型態參數化的函式樣板(function templates)。有了函式樣板, C++ 編譯器會依據函式呼叫時的引數資料型態來自動產生所需的函式定義。
定義函式樣板 函式樣板的定義格式如下: template <type parameter list> function definition 其中 type parameter list 的項目可以是: type parameter: typename type_parameter_name (ANSI C++) class type_parameter_name (舊式的 C++) nontype parameter: type constant_name 項目之間必須用逗號區隔開來。
範例: 以前面的 min 函式為例,我們可以寫出底下的函式樣板: template <typename T> T min (T x, T y) { return (x < y)? x : y } template <class T> T min (T x, T y) { return (x < y)? x : y } 或 其中的 T 是函式樣板的資料型態參數。你可以用任何合法的 C++ 名稱來取代 T,如 S, Type, …, 等等。
範例: 函式樣板的非資料型態參數是用來指定出現在函式樣板定義 中的常數值。 template <typename T, int size> T min (const T (&a)[size]) { T min_val = a[0]; for (int i = 1; i < size; i++) if (a[i] < min_val) min_val = a[i]; return min_val; }
In many ways, templates work like preprocessor macros, replacing the templated variable with the given type. However, there are many differences between a macro like this: #define min(i, j) (((i) < (j)) ? (i) : (j)) and a template: template<typename T> T min (T i, T j) { return ((i < j) ? i : j) } Here are some problems with the macro: There is no way for the compiler to verify that the macro parameters are of compatible types. The macro is expanded without any special type checking.
The i and j parameters are evaluated twice The i and j parameters are evaluated twice. For example, if either parameter has a post-incremented variable, the increment is performed two times. For example, after the evaluations: x = 5; y = 6; z = min(x++, y); variable x becomes 7, not 6. Because macros are expanded by the preprocessor, compiler error messages will refer to the expanded macro, rather than the macro definition itself. Also, the macro will show up in expanded form during debugging.
函式樣板的實例化 當呼叫函式樣板時,C++ 編譯器會依據引數的資料型態,自動產生出所需的函式,這個過程稱為實例化(instantiate)。 int m, x, y; m = min(x, y); template <typename T> T min (T x, T y) { return (x < y)? x : y } T int int min (int x, int y) { return (x < y)? x : y }
int min (const int (&a)[5]) { … } int ia[] = { 5, 2, 8, 3, 9}; int m = min(ia); 範例: T int, size 5 template <typename T, int size> T min (const T (&a)[size]) { T min_val = a[0]; for (int i = 0; i < size; i++) if (a[i] < min_val) min_val = a[i]; return min_val; } int min (const int (&a)[5]) { … } double da[] = { 5.0, 2.0, 8.3}; double m = min(da); T double, size 3 double min (const double (&a)[3]) { … }
把函式樣板指定給函式指標時,C++ 編譯器也會依據引數的資料型態,自動產生出所需的函式。 int (*f) (int, int); f = min; template <typename T> T min (T x, T y) { return (x < y)? x : y } T int int min (int x, int y) { return (x < y)? x : y }
Template Argument Deduction 如前所述,C++ 編譯器從函式樣板呼叫時的引數型態來產生真實的函式。如果引數的型態和函式樣板的參數型態並不一致時,C++ 編譯器就必須根據一些規則來推導出合適的具體函式。這個過程稱之為 Template Argument Deduction(樣板引數的演繹)。 底下我們用例子來說明這些演繹的規則。
演繹時只檢視引數的資料型態,而不考慮傳回值的資料型態。 template <typename T> T min (T x, T y) { return (x < y)? x : y } double d = min(3, 4); // 產生 int min (int, int) int m = min(3.0, 4.0); // 產生 double min (double , double ) int *ip = min(3, 4); // 產生 int min (int, int),但由於 int 不 // 可指定給 int *,所以此行會造成 // 編譯的錯誤。
如果引數資料型態與函式樣板的參數資料型態不一致,則可能會造成編譯上的錯誤。 template <typename T, int size> T min (const T (&a)[size]) { … } void f (int pval[9]) { // error: Type (&)[] != int * int jval = min(pval); }
template <typename T> T min (T x, T y) { return (x < y)? x : y } unsigned int ui; // error: cannot instantiate min(unsigned int, int) // must be: min(int, int) or // min(unsigned int, unsigned int) int m = min(ui, 1024);
由於陣列可視為指標的一種,因此 C++ 編譯器允許引數資料型態是陣列,而函式樣板的參數資料型態是指標。 template <typename T> T min (T *a, int size) { … } int ia[] = { 5, 2, 8, 3, 9}; int m = min(ia, 5); // 產生 int min(int *, int) double da[10]; double d = min(da, 10); // 產生 double min(double *, int)
演繹時不考慮函式樣板資料型態參數中的 const 和 volatile 宣告。 template <typename T> T min (const T *a, int size) { … } int *ia = new int[10]; int m = min(ia, 10); // 產生 int min(const int *, int) double *da = new double[10]; // 產生 double min(const double *, int) double d = min(da, 10);
Explicit Template Arguments 當無法決定如何從函式樣板產生具體函式時、或需要自定產生某種資料型態的具體函式時,我們可以在呼叫函式樣板時,明白地指定出資料型態參數的種類。指定的格式如下: template_func<explicit type list>(argument list); 譬如: template <typename T> T min (T x, T y) { return (x < y)? x : y } unsigned int ui; // min(unsigned int, unsigned int) unsigned int m = min<unsigned int>(ui, 1024);
範例: template < typename RT, typename T1, typename T2> RT sum (T1 x, T2 y) { … } typedef unsigned int ui_type; char ch; ui_type ui; // error: RT can’t be deduced. ui_type loc1 = sum(ch, ui); // ok: ui_type sum(char, ui_type) ui_type loc1 = sum<ui_type, char, ui_type>(ch, ui);
範例: template < typename RT, typename T1, typename T2> RT sum (T1 x, T2 y) { … } void manipulate(int (*pf)(int, char)); void manipulate(double (*pf)(float, float)); // error: which instantiation of sum? manipulate(sum); // ok manipulate(sum<double, float, float>);
若不會造成混淆的話,< 和 > 之間的資料類型有些可以省略。 template < typename RT, typename T1, typename T2> RT sum (T1 x, T2 y) { … } typedef unsigned int ui_type; char ch; ui_type ui; // ok: ui_type sum(char, ui_type) // T1 和 T2 可由引數的資料型態推演得到 ui_type loc1 = sum<ui_type>(ch, ui); // T1 和 T2 可由 pf 的引數資料型態推演得到 ui_type (*pf)(char, ui_type) = sum<ui_type>;
Template Compilation Models Inclusion Compilation 把函式樣板的定義寫在 .h 標頭檔之中,然後在呼叫函式樣板的程式檔中把這個標頭檔用 include 的方式加進來,讓 C++ 編譯器得以進行函式樣板的 instantiation。 Separation Compilation 把函式樣板的定義寫在一個 .cpp 程式檔之中,然後在呼叫函式樣板的程式檔中宣告此函式樣板。 C++ 編譯器會在編譯後,自動進行所需的 instantiation。 Explicit Instantiation Declaration 由程式設計師指定所需的 instantiation。
Inclusion Compilation Model 把函式樣板的定義寫在 .h 標頭檔之中,然後在呼叫函式樣板的程式檔中用 include 的方式把這個標頭檔加進來。譬如: // file: min.h template <typename T> T min (T x, T y) { return (x < y)? x : y } // prog.cpp #include “min.h” … int m = min(3, 4); 這個方法的優點是:C++ 編譯器可以直接地利用標頭檔中的函式樣板定義來進行 instantiation 的步驟。此外,現有的 C++ 編譯器絕大部份都支援這個方法。
上述方法的缺點是: 當有多個程式檔 include 定義函式樣板的 .h 標頭檔時,C++ 編譯器會重複多次的 instantiations,產生出許多個相同的函式定義,然而其中只有一個被保留下來,其它則捨棄不用。這顯然不是一個有效率的作法。 把函式樣板的定義寫在 .h 標頭檔而暴露在外,顯然不符合所謂 information hiding 的原則。 若函式樣板的定義又臭又長,又有多個程式檔 include 定義它的標頭檔時,C++ 編譯器必須重複地處理,無疑地也會增加 C++ 編譯器許多的負擔。
Separation Compilation Model 把函式樣板的定義寫在一個 .cpp 程式檔之中,然後在呼叫函式樣板的程式檔中宣告此函式樣板。 // file: min.h template <typename T> T min (T x, T y) ; // declaration // prog.cpp #include “min.h” … int m = min(3, 4); // file: min.cpp export template <typename T> T min (T x, T y) { return (x < y)? x : y } 注意行首的 關鍵字 export
上述的方法雖然解決了 inclusion compilation 的缺點,然而許多不符合 ANSI 標準的舊 C++ 編譯器並不支援這個方法。儘管如此,只要你的 C++ 編譯器有支援的話,你還是應該使用這項 ANSI C++ 的新功能。
Explicit Instantiation Declaration template <typename T> T sum (T op1, int op2) { … } // explicit instantiation declaration template int* sum<int *> (int *, int); 產生 int* sum(int *, int);
Template Explicit Specialization template <typename T> T min (T x, T y) { return (x < y)? x : y } 因此我們就寫一個特殊化的函式樣板如下: template<> const char * min (const char *x, const char * y) { return strcmp(x,y) < 0? x : y; }
template <typename T> T min (T x, T y) { return (x < y)? x : y } template<> const char * min (const char *x, const char * y) { return strcmp(x,y) < 0? x : y; } int d = min(2.0, 3.0); // 產生 double min(double, double) int m = min(2, 3); // 產生 int min(int , int ) // 產生 const char * min(const char *, const char *) const char *cp = min(“book”, “apple”);
Overloading Function Templates 如一般函式,我們也可以定義超載函式樣板。譬如: template <typename T> T min(const vector<T>&); // #1 T min(const T*, int); // #2 T min(T, T); // #3 // ordinary function const char *min(const char *, const char *); // #4
vector<int> iv(1024); int m1 = min(iv); // #1 instantiation int ia[1024]; int m2 = min(ia, 1024); // #2 instantiation double d = min(3.0, 4.0); // #3 instantiation float f = min(3.0, 4.0); // #3 instantiation int m3 = min(3, 4); // #3 instantiation const char *cp = min(“Y”, “N”); // #4 instantiation unsigned ui; int m4 = min(3, ui); // error
範例: template <typename T> T min(T, T); template <typename T, typename U> T min(T, U); unsigned ui; int m4 = min(3, ui); // ok int m3 = min(3, 4); // error: ambiguous
範例: template <typename T> // #1, more general T sum(T, int); template <typename T> // #2, more special T sum(T*, int); int ia[1024]; int m2 = sum(ia, 1024); // #2 instantiation