- Back to Home »
- CPP »
- C++ Templates
C++ Templates
Reference to
+ link 1
+ link 2
+ link 3
1. Template là gì ?
Template (Khuôn hình - Mẫu ) là một từ khóa của C++ đặc trưng cho việc tổng quát hóa việc xử lý với các kiểu dữ liệu khác nhau.
Template được sử dụng để "lập trình tổng quát" (generic programming). C++ có một thư viện rất mạnh mà các bạn chắc cũng biết là Standard Template Library được xây dựng hoàn toàn dựa trên template.
Template thông báo cho trình biên dịch biết rằng các hàm, các lớp, các xử lý liên quan sẽ được quyết định sử dụng kiểu dữ liệu nào cho phù hợp trong complie_time (quá trình biên dịch).
Khi đó trình biên dịch sẽ "để lại" các xử lý đó tới khi có dữ liệu thực sự được tạo ra nhưng sẽ tạo ra một khuôn mẫu chung cho các xử lý. Khi xử lý đó được gọi với những kiểu dữ liệu đã xác định thì trình biên dịch lúc đó sẽ tự biết dùng xử lý nào với kiểu dữ liệu được truyền vào này một cách phù hợp nhất.
Ưu điểm của việc sử dụng template :
- Tổng quát hóa các trường hợp để xử lý, không cần thiết phải xử lý các trường hợp có chung đặc điểm (generic programming) >> Tối ưu hóa mã nguồn, tránh lặp lại xử lý.
Cũng có một số trường hợp ta có thể sử dụng function object (đối tượng (là) hàm) nhưng việc sử dụng template phổ biến và tiện dụng hơn.
2. Ví dụ cơ bản về việc sử dụng template
Giả sử ta có hai hàm tìm số lớn hơn trong hai số
// Voi kieu int
int maxInt (int a, int b) {
return (a>b?a:b);
}
// Voi kieu double
int maxDouble (double a, double b) {
return (a>b?a:b);
}
Ta thấy cả hai hàm kia đều có chung một cách giải quyết vấn đề. Điểm khác biệt ở đây chỉ là kiểu dữ liệu được truyền vào và được trả về của từng hàm. Vậy ta có cách nào gộp chung 2 hàm làm 1 hàm không? Có hai cách giải quyết ở đây :
- Cách 1 : Chỉ dùng hàm maxDouble vì dễ thấy rằng ta có thể truyền được 2 số nguyên kiểu int vào hàm. Nhưng giá trị trả về lại là kiểu double >> Cách này vẫn ổn, nhưng kiểu dữ liệu trả về lại không như ta mong muốn.
- Cách 2 : Sử dụng template để "gom" hai hàm này thành 1 hàm duy nhất. Khi sử dụng ta sẽ gán kiểu phù hợp cho nó.
template <class T> // có thể dùng từ khóa typename thay cho class ở đây với ý nghĩa tương đương. T maxNumber (T a, Tb) { return (a>b?a:b); } // Việc sử dụng class hay typename sẽ được phân tích trong phần sau.Ở đây ta đã "gom" hai kiểu dữ liệu double và int thành một kiểu dữ liệu duy nhất - kiểu "T". Việc này có nghĩa là ta sẽ thao tác với kiểu dữ liệu trừu tượng T này chứ không thao tác với một kiểu dữ liệu cụ thể nào nữa.
Sử dụng :
int main () { int a = 10; int b = 5; cout << maxNumber<int> (a,b) << endl; double c = 3.3; double d = 2.0; cout << maxNumber<double> (c,d) << endl; return 0; }Ta sử dụng hàm maxNumber :
maxNumhber <int> (a,b);
và
maxNumber <double> (c,d);
Khi này hai kiểu dữ liệu cụ thể được ta sử dụng là int và double chứ không còn là một kiểu dữ liệu trừu tượng T nữa.
3. Function Templates
Function Template - Khuôn hình hàm (mẫu hàm) giúp ta xây dựng các hàm tổng quát.
Ví dụ phía trên : hàm maxNumber cũng là một function template.
Bây giờ mình sẽ nói kĩ hơn một chút về function template qua các ví dụ.
3.1 Sử dụng function template cho các hàm với các kiểu dữ liệu khác nhau
template <class T, class U, class V> T function (T a, U b, V c) { // to do something. };Các kiểu dữ liệu T, U, V ở đây có thể là bất kì kiểu dữ liệu nào :
- Kiểu dữ liệu cơ bản như : int, char, usigned int, double ....
- Kiểu dữ liệu do người dùng định nghĩa ví dụ như typedef vector<char> charVector; .....
- Các lớp do người dùng tạo ra class myClass () {}; ....
Danh sách các kiểu T, U, V ... được gọi là parameters of template (mẫu tham số).
Sau đây là một ví dụ về sử dụng function template với nhiều kiểu dữ liệu khác nhau.
template <class T, class U, class V> V isMyName (T name, U yearOld, V true_or_false = true) { if (name == "Coco Rude" && yearOld == 20) { return true_or_false; } else return false; } int main () { string name = "Coco Rude"; int yearOld = 20; cout << isMyName <string, int, bool> (name, yearOld, true) << endl; return 0; } // Output 1Qua ví dụ trên ta thấy được function template có thể được sử dụng với nhiều đối số thuộc các kiểu khác nhau. Tuy nhiên có một điều hết sức chú ý ở đây là các xử lý liên quan tới các kiểu dữ liệu phải phù hợp.
Ví dụ như ta lại dùng hàm isMyName <string, string, string> (....) thì sẽ xảy ra lỗi trong quá trình so sánh yearOld - đối số thứ 2 với giá trị 20.
Vì thế sự phù hợp của các kiểu dữ liệu với phép toán ta sử dụng (hay nói tổng quát là với các xử lý trên kiểu dữ liệu đó) là hết sức quan trọng để tránh gây lỗi và gây hiểu lầm cho người sử dụng hàm.
3.2 Overload (nạp chồng)
Các hàm được viết dưới dạng template đều có thể được nạp chồng như những hàm bình thường khác.
Ví dụ như sau :
template <class T> T maxNumber (T a, T b) { return (a>b?a:b); }; //Overload maxNumber function for three number template <class T> T maxNumber (T a, T b, T c) { T temp = (a>b?a:b); return (temp>c?temp:c); }; int main (int argc, char *argv[]) { int a = 10; int b = 15; int c = 30; cout << "max a b : " << maxNumber <int> (a, b) << endl; cout << "max a b c : " << maxNumber <int> (a, b, c) << endl; return 0; } // Ouput max a b : 15 max a b c : 30Trên đây là 1 dạng nạp chồng với số đối số khác nhau.
Cũng có thể nạp chồng hàm với các cách khác như thay đổi các đối số hay giá trị trả về ...
template <class T, class V) T maxNumber <T a, T b, V c) { // to do something. } // Hay template <class T, class V) V maxNumber <T a, T b, T c) { // to do something. } // ..v.v..4. Class Templates
4.1 Khái niệm, ý tưởng
Class Template (Khuôn hình lớp) là một trường hợp trong việc xây dựng các template với ý tưởng chính cho việc tạo nên các class template là hợp chung của 2 ý tưởng :
- Class : Nhóm các dữ liệu khác kiểu vào với nhau thành 1 lớp; bao gói dữ liệu và việc xử lý dữ liệu đó vào cùng một chỗ. Tăng tính bảo mật cho dữ liệu cũng như abstraction (trừu tượng hóa) dữ liệu với người dùng.
- Template : Gom các xử lý với nhiều kiểu dữ liệu khác nhau về cùng một chỗ để tránh việc viết lại code và tổng quát hóa quá trình xử lý dữ liệu.
>> Tựu chung hai ý tưởng này làm 1 ta sẽ thấy được việc vì sao lại xây dựng các class template.
4.2 Ví dụ cơ bản về sử dụng class template
Ví dụ thông dụng nhất về việc xây dựng các class template là xây dựng các cặp (pair).
- Cặp <string, string> trong từ điển - <từ, nghĩa của từ>
- Cặp <int, string> trong quản lý danh sách sinh viên - <mã số sinh viên, họ tên>
........
Ta xây dựng một class tempalte
#include <iostream> using namespace std; template <class T, class V> class myPair { public : myPair (T first, V second) : first(first), second (second) { // no to do something }; // get method T getFirst (); V getSecond (); private : T first; V second; }; // MAIN int main (int argc, char *argv[]) { myPair<int, int> *pair1 = new myPair<int, int> (1,2); cout << "First : " << pair1->getFirst() << " | " << "Second : " << pair1->getSecond() << endl; myPair<int, string> *pair2 = new myPair<int, string> (10020273, "Coco Rude"); cout << "First : " << pair2->getFirst() << " | " << "Second : " << pair2->getSecond() << endl; myPair<string, string> *pair3 = new myPair<string, string> ("Coco Rude", "Trai dua tho lo =))"); cout << "First : " << pair3->getFirst() << " | " << "Second : " << pair3->getSecond() << endl; delete pair1; delete pair2; delete pair3; return 0; } // DEFINE Class's methods template <class T, class V> T myPair<T,V>::getFirst () { return this->first; }; template <class T, class V> V myPair<T,V>::getSecond () { return this->second; };Ta gộp 2 kiểu T và V thành 1 class chứa 2 kiểu đó và tạo các xử lý với 2 kiểu trên.
Tạo class gồm 2 hay nhiều kiểu dữ liệu như sau :
template <class T, class V, ......> class myClass { // to do something }Định nghĩa phương thức myMethod (T a, V b ...) bên ngoài class thì ta phải thêm danh sách các kiểu vào trước như sau :
template <class T, class V ....> T myClass <T, V, ....> :: myMethod (T a, V b, ....) { // to do something }Câu hỏi đặt ra là vì sao đã thêm danh sách các kiểu ở template <class T, class V, ....> mà phía dưới lại phải thêm danh sách các kiểu cho class myPair <T, V, ...>. Việc này khiến chúng ta rối lên, dẫn tới nhiều khi quên hay nhầm lẫn.
Câu trả lời là : Các bước khi trình biên dịch biên dịch chương trình
- Trình biên dịch khi biên dịch sẽ nhìn vào danh sách các kiểu ở lời gọi : pair1->getFirst() .....
- Nhìn vào danh sách các kiểu khi đối tượng được khởi tạo. myPair<int, int> ....
- Nhìn vào nơi khai báo hàm (hay phương thức đó) : myPair <T, V> từ đó xác định kiểu dữ liệu trả về cũng như các tham số của hàm : T getFirst(); (ID của hàm).
- Nhìn vào định nghĩa hàm để xác định danh sách các kiểu + danh sách đối số + kiểu dữ liệu trả về xem có phù hợp với mẫu của lớp đó không để sau đó thực thi hàm.
Vì vậy khi khai báo hàm bên ngoài lớp ta nên hết sức chú ý tới :
- Danh sách kiểu của lớp đó. template <class T, class V,....>
- Kiểu trả về : T, V hoặc kiểu khác.
- Tên lớp cùng template (mẫu của lớp đó) : myPair <T, V, ...>
- Tên và đối số của hàm (phương thức) : myMethod (T a, V b, ....)
Khi sử dụng ta phải xác định đầy đủ các đối số (nếu không có đối số nào được định trước).
// Đơn giản như sau : myPair <int, int> pair0 (100,100); // Hay phức tạp hơn với con trỏ. myPair<int, int> *pair1 = new myPair<int, int> (1,2); myPair<int, string> *pair2 = new myPair<int, string> (10020273, "Coco Rude"); myPair<string, string> *pair3 = new myPair<string, string> ("Coco Rude", "Trai dua tho lo =))");
5. Template Specialization
5.1 Vì sao cần Template specialization ?
Template specialization - Mẫu chuyên môn hóa (cái này không nên dịch) được tạo ra khi ta cần chuyên môn hóa function template hay class template cho một (hay nhiều) kiểu nhất định.
5.2 Ví dụ cụ thể
Ta có một class template để cộng hai đối tượng thuộc cùng một kiểu như sau :
template <class T> class plusTwoElement { public : plusTwoElement (T a, T b) : a(a), b(b) { // to do something }; T plus () { return a + b; }; private : T a, b; };Nhưng ta lại muốn kiểu char ta không cần hàm cộng 2 kí tự mà cần một hàm để upper 2 kí tự >> ta cần tạo một class template riêng cho kiểu char. Class template này được gọi là class template specialization (mẫu lớp chuyên môn hóa).
template <> class plusTwoElement <char> { public : plusTwoElement (char a, char b) : a(a), b(b) { // to do something }; void upperChar () { if ((a>='a')&&(a<='z')) a+='A'-'a'; if ((b>='a')&&(b<='z')) b+='A'-'a'; cout << a << " | " << b << endl; }; private : char a, b; };Sử dụng 2 class template này :
int main (int argc, char *argv[]) { plusTwoElement<int> pl1 (1,2); plusTwoElement<char> pl2 ('a', 'b'); cout << pl1.plus() << endl; pl2.upperChar(); return 0; }Ta đã chuyên môn hóa class template cho kiểu char.
Điểm khác biệt của template specialization với các class (function) khác là câu lệnh khi khởi tạo class :
template<> // Ta không đưa danh sách các kiểu class plusTwoElement <char> { // Chỉ rõ kiểu cụ thể của class ở đây là char. // to do something } // Khi định nghĩa các phương thức của class bên ngoài // Ta không sử dụng từ khóa template trước định nghĩa phương thức đó nữa // thay vào đó ta chỉ rõ kiểu cụ thể của class. void plusTwoElement <char> :: upperChar () { // to do something. }>> Vậy khi nào thì ta sử dụng template specialization :
- Khi bạn tạo một class (function) template xử lý chung một loạt các kiểu khác nhau nhưng lại muốn xử lý một kiểu cụ thể nào đó với cách khác.
- Khi các xử lý chung chung không đáp ứng được cho 1 kiểu riêng biệt.
>> Khi đó bạn cần tạo một template specialization cho một (hay nhiều) kiểu cụ thể.
6 Non-type parameters for templates
6.1 Non-type parameters for templates ?
Non-type parameters for templates : Mẫu không có tham số template.
Là một class (function) template có tham số kiểu bao gồm cả kiểu T chung và kiểu cụ thể nào đó.
6.2 Ví dụ
Ta muốn duyệt một mảng các kiểu nào đó, khi đó cần (nên) có một đối số là độ dài của mảng đó.
template <class T, int size> class myArray { public : myArray () { // to do something }; void setElement (int index, T value) { array[index] = value; }; T getElement (int index) { return array[index]; }; private : T array [size]; }; int main (int argc, char *argv[]) { myArray<int, 5> intAr; intAr.setElement(0,100); cout << intAr.getElement(0) << endl; return 0; }Ở đây ta thấy kiểu của size là int đã được định rõ từ trước.
template <class T, int size> class myArray { // };Template bao gồm cả kiểu T bất kì và kiểu int xác định.
Khi định nghĩa các phương thức bên ngoài lớp thì danh sách kiểu cũng giống như danh sách kiểu của class khi khai báo.
template <class T, int size> void myArray <T, size> :: setElement (int index, T value) { // to do something }>> Khi nào thì ta sử dụng template có kiểu được định trước ??
- Khi ta đã biết trước một hay nhiều kiểu trong danh sách các kiểu của template. Ví dụ như độ dài của mảng là kiểu int, điểm của một sinh viên là kiểu double, tên của người là kiểu string .....
Khi đó ta xây dựng các template mà trong đó có một số kiểu đã được xác định trước là int, double hay string ...
Tất nhiên việc để các kiểu (int, double, string ...) là một kiểu V nào đó vẫn được nhưng khi đó người sử dụng template của bạn có thể gặp khó khăn trong việc xác định xem template của bạn là nhiệm vụ gì.
>> Chuyên hóa template của bạn cho một công việc được xác định trước.
7. Template trong project
Một vấn đề mà mình và rất nhiều bạn gặp phải khi sử dụng template trong các project đó là việc phân tách file mã nguồn. Mình sẽ bàn qua một chút về vấn đề này.
Trong project tất nhiên việc phân tách file mã nguồn là hoàn toàn hợp lý khi bạn muốn quản lý chương trình của bạn dễ dàng hơn. Nhưng vấn đề gặp phải là trình biên dịch của bạn sẽ gặp rắc rối với template trong các file header.
Lấy lại ví dụ phía trên :
// File myPair.h template <class T, class V> class myPair { public : myPair (T first, V second) : first(first), second (second) { // no to do something }; // get method T getFirst (); V getSecond (); private : T first; V second; };
// File myPair.code // Dinh nghia cac phuong thuc cua class myclass trong file myPair.h #include <iostream> #include "myPair.h" using namespace std; template <class T, class V> T myPair<T,V>::getFirst () { return this->first; }; template <class T, class V> V myPair<T,V>::getSecond () { return this->second; };
// file main.code #include <iostream> #include "myPair.h" using namespace std; // MAIN int main (int argc, char *argv[]) { myPair<int, int> *pair1 = new myPair<int, int> (1,2); cout << "First : " << pair1->getFirst() << " | " << "Second : " << pair1->getSecond() << endl; delete pair1; return 0; }Khi biên dịch bạn sẽ gặp một thông báo lỗi kiểu như sau :
quannh@uet:~/ITVA$ make g++ main.o myPair.o -o run main.o: In function `main': main.code:(.text+0x3d): undefined reference to `myPair<int, int>::getSecond()' main.code:(.text+0x4b): undefined reference to `myPair<int, int>::getFirst()' collect2: error: ld returned 1 exit status make: *** [run] Error 1Hai dòng thông báo :
undefined reference to `myPair<int, int>::getSecond()' undefined reference to `myPair<int, int>::getFirst()'Nghĩa là ta chưa định nghĩa class myPair cho trường hợp danh sách tham số là <int, int>. Vì sao lại vậy?? (WTF?) Ta đã định nghĩa template rồi cơ mà, sao trình biên dịch lại không tạo ra được class myPair với kiểu <int, int> ??
Câu trả lời là quá trình biên dịch của trình biên dịch phổ biến (gcc/g++) như sau :
- Tại nơi gọi hàm getFirst() trong main, trình biên dịch nhìn vào danh sách tham số kiểu để đi tìm trong file myPair.h, từ đó nó tìm các định nghĩa của phương thức này trong myPair.code
- Nhưng nó lại không thấy trong file myPair.code định nghĩa nào cho myPair<int, int> :: getFirst() mà chỉ thấy myPair<T, V> :: getFirst ();
Nó (trình biên dịch) không "hiểu" được khi đó ta đang tạo ra đối tượng thuộc lớp myPair với kiểu T là int, V là int.
Không giống như thông thường ta để các định nghĩa của các phương thức (hàm) và khai báo của nó trong cùng một file mã nguồn, khi đó nó có thể "hiểu" được cùng lúc đó ta tạo ra một phương thức với kiểu T = int , V = int.
>> Nói tóm lại :
- Trình biên dịch phải nhìn thấy cả prototype (khai báo) và define (định nghĩa) của một phương thức template.
- Nó không nhớ chi tiết của các tập tin .code trong khi biên dịch tập tin nên không thể biết được kiểu nào sẽ được sử dụng cho template.
Vậy làm cách nào để biên dịch được? Có 3 cách cho vấn đề này :
** Cách 1 : Đưa tât cả khai báo và định nghĩa và cùng một tập tin : Ví dụ đưa nội dung các định nghĩa của myPair.code và trong myPair.h.
- Lợi thế : Ta không phải chỉ ra tất cả các kiểu sẽ được dùng với template.
- Nhược điểm : Việc này sẽ làm tăng kích thước tập tin header >> làm mất đi tính che giấu định nghĩa >> khó quản lý chương trình.
Cách này có còn một nhược điểm là bạn không thể compile trước định nghĩa thành file object riêng để phát hành kèm với header. Điều này dẫn tới
Ưu điểm: Chỉ cần include file header vào là dùng được luôn, không cần setup linker như khi sử dụng thư viện compile sẵn.
Nhược điểm: Mỗi lần compile chương trình thì phải compile lại cả thư viện => tốn thời gian
Cái này có thể giải quyết bằng precompiled-header nhưng mà lại phụ thuộc vào compiler support, mà cũng thấy ít người dùng
** Cách 2 : Đưa ra danh sách các trường hợp bạn sẽ dùng (hay có thể sẽ dùng) trong khi định nghĩa các phương thức trong file .code
// File myPair.code // Dinh nghia cac phuong thuc cua class myclass trong file myPair.h #include <iostream> #include "myPair.h" using namespace std; template <class T, class V> T myPair<T,V>::getFirst () { return this->first; }; template <class T, class V> V myPair<T,V>::getSecond () { return this->second; }; // Đưa ra danh sach các trường hợp ta có thể dùng. template class myPair<int, int>; template class myPair<int, string>; // ...v.v....** Cách 3 : Ta tách các trường hợp sử dụng trong cách 2 ra một file riêng biệt. (có thể đặt cùng chỗ với file định nghĩa hoặc nơi khác).
// File "myPair-impl.code" #include "myPair.code" // Đưa ra danh sach các trường hợp ta có thể dùng. template class myPair<int, int>; template class myPair<int, string>; // ...v.v....Các bạn có thể đọc thêm ở đây phân tích nguyên nhân và cách khắc phục khá đầy đủ :
http://www.parashift....html#faq-35.12
8. Bàn về việc sử dụng hai từ khóa typename và class
Trong định nghĩa template ta có thể dùng 2 từ khóa typename và class thay thế cho nhau. Vậy vì sao lại có sự trùng lặp này.
Vấn đề thứ nhất là do vấn đề lịch sử. Khi ngài Stroustrup viết đặc tả template thì ngài muốn sử dụng lại từ khóa class để đỡ phải định nghĩa một từ khóa mới. Một số người thích dùng typename vì nó đặc trưng cho 1 kiểu hơn từ khóa class và có thể đỡ nhầm lẫn khi viết các class template (nhầm lẫn giữa các từ class với nhau). Một số người khác thì lại thích dùng từ khóa class vì nó "gõ nhanh hơn" . Vấn đề sợ nhầm lẫn trên có thể được giải quyết bằng cách xuống dòng kiểu như sau
template <class T, class V ...> class myClass { // } // Hoặc dùng typename template <typename T, typename V ...> class myClass { // }Nhưng lý do chính đáng nhất có lẽ là do trường hợp dưới đây.
Giả sử kiểu T có một kiểu con (lớp trong) tên là subT.
class T { public : class subT () { // } }Khi đó trong template sử dụng kiểu T, ta muốn có một thành phần mang kiểu subT của T. Khi đó ta phải sử dụng từ khóa typename
template <class T> class myClass { private : typename T::subT *pointer; // con trỏ poiter có kiểu subT của T. }Nếu không có từ khóa typename thì trình biên dịch sẽ mặc định coi subT là một biến static của T. Và khi đó T::subT *poiter sẽ không phải là một con trỏ kiểu subT của T nữa mà là tích của biến tĩnh subT trong T với biến pointer được định nghĩa ở đâu đó. >> Trình biên dịch sẽ báo lỗi không tìm thấy biến static subT của T và không tìm thấy biến pointer.
Tóm lai :
Vậy khi nào thì ta nên sử dụng template :
- Khi ta muốn xây dựng một lớp hay một hàm xử lý những công việc chung cho nhiều kiểu khác nhau.
- Mục tiêu của việc tổng quát hóa lập trình là khiến người sử dụng lớp hay hàm đó không phải bận tâm về các kiểu khác nhau. Khi đó công việc "ẩn" đi các xử lý với các kiểu khác nhau được đẩy cho người viết hàm hay lớp tổng quát đó.
- Công việc sử dụng lại mã sẽ hết sức đơn giản. Một ví dụ rất rõ ràng chính là bộ thư viện template của C++. Các bạn có thể sử dụng các công cụ của nó mà nhiều khi không cần quan tâm nhiều tới việc nó được tạo ra như thế nào. Ví dụ như toán tử xuất "<<". Các bạn có thể xuất 1 số kiểu int, double, float,... hay một kí tự char, chuỗi string .... mà chỉ cần quan tâm là toán tử đó hỗ trợ kiểu của bạn. Không cần quan tâm nó được xuất ra sao, được định nghĩa như thế nào.