Template là gì?
Template là từ khóa trong C++, có thể hiểu rằng là nó 1 kiểu dữ liệu trừu tượng, đặc trưng cho các kiểu dữ liệu cơ bản. Template là từ khóa báo cho trình biên dịch rằng đoạn mã sau đây định nghĩa cho nhiều kiểu dữ liệu và mã nguồn của nó sẽ được compile sinh ra tương ứng cho từng kiểu dữ liệu trong quá trình biên dịch.
Có 2 loại template cơ bản:
- Function template: là 1 khuôn mẫu hàm, cho phép định nghĩa các hàm tổng quát thao tác cho nhiều kiểu dữ liệu.
- Class template: là 1 khuôn mẫu lớp, cho phép định nghĩa các lớp tổng quát cho nhiều kiểu dữ liệu.
Function template
void Swap( int &x, int &y) { int Temp = x; x = y; y = Temp; }
Với đoạn code trên dùng để hoán vị giữa 2 số nguyên, nếu cần hoán vị giữa 2 số kiểu float
hoặc double
,… thì lại phải định nghĩa các hàm cho từng loại kiểu dữ liệu như thế. Có cách nào mà có thể định nghĩa 1 hàm duy nhất mà có thể dùng cho nhiều kiểu dữ liệu hay không, sau 1 hồi tìm hiểu thì câu trả lời đó là template
, với từ khóa template
chỉ cần định nghĩa 1 hàm duy nhất cho các kiểu dữ liệu: int
, float
, double
, …
template <class Type> void Swap( Type &x, Type &y) { Type Temp = x; x = y; y = Temp; } void main() { int x = 5, y = 10; float a = 5.5f, b = 3.0f; Swap(x, y); Swap(a, b); }
Vậy cách thức nó hoạt động ra sao, hãy xét hoạt động của trình biên dịch khị gặp lời gọi hàm Swap(x, y)
với tham số truyền vào là kiểu int
, trước hết khi gặp lời gọi hàm Swap(x, y)
thì trình biên dịch tìm xem có hàm Swap()
nào đã được khai báo với tham số kiểu int
hay chưa, nếu có thì sẽ liên kết với hàm đó, nếu chưa nhưng lại tìm thấy từ khóa “template
” với hàm Swap()
được truyền vào 2 tham số cùng kiểu với nhau (lúc này là kiểu Type
), trình biên dịch chỉ cần kiểm tra xem lời gọi hàm Swap(x, y)
có 2 tham số có cùng kiểu dữ liệu với nhau hay không( trong ví dụ trên là 2 tham số kiểu int
-> cùng kiểu dữ liệu), nếu cùng kiểu thì trình biên dịch lại kiểm tra xem hàm Swap()
với 2 tham số kiểu int
đã được sinh ra trước đó hay chưa, nếu có thì lời gọi hàm sẽ liên kết với hàm Swap()
đã được sinh ra, nếu chưa thì khi đó trình biên dịch sẽ sinh ra hàm Swap(int &x, int &y)
, tương tự với Swap(a, b)
kiểu float
cũng tương tự như vậy. Vì vậy trong quá trình biên dịch sẽ sinh ra 2 hàm Swap()
cho 2 loại kiểu dữ liệu trên.
Trong ví dụ trên đã chỉ ra cách khai báo hàm mẫu, trong đó:
Type
chỉ là 1 tên riêng thể hiện cho 1 kiểu dữ liệu tổng quát.class
có thể thay thế bằngtypename
, ở đây nó không có sự khác biệt.
Ngoài ra có thể dùng prototype cho nguyên mẫu hàm giống như làm cho các hàm thông thường
template <class T> void Swap( T &x, T &y); void main() { int x = 3, y = 4; float a = 1.2f, b = 2.2f; Swap(x, y); Swap(a, b); } template <class T> void Swap( T &x, T &y) { T z = x; x = y; y = z; }
Trong ví dụ trên chỉ hoán ví giữa 2 tham số cùng kiểu, vậy với khác kiểu thì sau? Chỉ cần khai báo thêm 1 kiểu dữ liệu tổng quát:
template <class T, class X> void Swap( T &x, X &y);
Overloading Function Templates
Nguyên mẫu hàm (function template) đều có tính chất của 1 hàm thông thường, nó cho phép nạp chồng (overload function)
template <class T, class X> T Sum( T x, X y) { T sum = x + y; return sum; } template <class T, class X> X Sum( T x, X y, T z) { X sum = x + y + z; return sum; }
Lưu ý: muốn nạp chồng hàm thì các tham số truyền vào ở các hàm phải khác nhau
Class template
Xét ví dụ sau
class Point { int x; int y; };
Nếu muốn khai báo khuôn mẫu lớp với kiểu tùy ý làm như sau
template <class Type> class Point { Type x; Type y; };
Lúc này nếu muốn khai báo 1 thể hiện template Point
Point<int> P1;
Trong ví dụ trên là khai báo Point
với 2 thuộc tính cùng kiểu với nhau, có thể định nghĩa với 2 thuộc tính có kiểu dữ liệu khác nhau
template <class TypeOne, class TypeTwo> class Point { TypeOne x; TypeTwo y; public: Point(); }; template <class TypeOne, class TypeTwo> Point<TypeOne, TypeTwo>::Point() { x = 0; y = 0; }
Trong ví dụ trên, khi muốn định nghĩa hàm của 1 lớp mẫu ở file khác thì phải khai báo template
với các tham số kiểu trước mỗi hàm muốn định nghĩa None-type template parameters. None-type template parameters là tham số mẫu mặc định, được xác định và trình biên dịch không sinh ra kiểu khác trong suốt thời gian biên dịch.
template <class Type, int N> class Stack { //do something };
Trong ví dụ trên N
là kiểu int
đã được xác định rõ từ trước và không thay đổi trong thời gian biên dịch.
Default type
Xét ví dụ sau
template <class Type = int> //defaults to type int class Point { Type x; Type y; }; Point<> point;
Trong ví dụ trên khi khai báo thể hiện của khuôn mẫu lớp là Point<> point;
do không có kiểu truyền vào, lúc này trình biên dịch sẽ mặc định là kiểu đó là int
do lúc đầu đã gán tham số kiểu mặc định là int
khi khai báo kiểu trong template
.
Lưu ý: tham số kiểu mặc định phải nằm ngoài cùng bên phải trong danh sách các tham số kiểu mẫu
Template và Friend
Như thường thấy các lớp và các hàm có thể được khai báo là friend của nhau. Đối với khuôn mẫu lớp cũng vậy, có thể khai báo friend giữa khuôn mẫu lớp và hàm toàn cục, hoặc hàm của các lớp khác,..
template <class Type> class Point { public: friend void Sum(Type X); friend void Sum(); }; template <class Type> void Sum(Type X) { printf("a friend of only a class template with same type\n"); } void Sum() { printf("a friend of every class template\n"); }
Trong ví dụ trên thì hàm Sum()
sẽ là hàm friend với Point<int>
, Point<float>
,… còn nếu Type
là int
thì lúc này Sum(int X)
là hàm friend với Point<int>
nhưng sẽ không là friend với Point<float>
Template và thành viên tĩnh
Tương tự như nontemplate class, có thể định nghĩa thành viên tĩnh trong khuôn mẫu lớp, và thành viên tĩnh phải được khởi tạo ở phạm vi toàn cục. Nhưng thành viên tĩnh trong khuôn mẫu lớp khác với nontemplate class là thời điểm khởi tạo, đối với nontemplate class thì sẽ được khởi tạo khi bắt đầu chương trình, nhưng còn đối với khuôn mẫu lớp thì lại khởi tạo trong thời gian biên dịch.
template <class Type> class Point { public: static int N; }; Template <class Type> int Point<Type>::N = 0; void main() { Point<int> t1; t1.N++; Point<float> t2; t2.N++; Point<int> t3; t3.N++; printf("T1: %d\n", t1.N); printf("T2: %d\n", t2.N); printf("T3: %d\n", t3.N); }
Kết quả in ra sẽ là 2 1 2
. Nguyên nhân là khi khai báo Point <int> t1;
lúc này trình biên dịch tìm xem Point<int>
đã được khởi tạo chưa, lúc này chưa thì trình biên dịch sẽ khởi tạo ra đối tượng Point
với kiểu cụ thể là int
, thành viên tĩnh N
được khởi tạo từ đây, sau khi thực thi dòng t1.N++;
lúc này N = 1
.
Đối với Point<float> t2
cũng tương tự như với Point<int> t1
, kết quả khi thực thi dòng t2.N++;
lúc này N = 1
. Đối với Point<int> t3
, lúc này lớp Point
với kiểu int
đã được khởi tạo rồi, thành viên tĩnh N
của t3
được sử dụng chung với t1
, lúc này khi t3.N++;
thì N = 2
. Vì vậy khi in kết quả lên màn hình sẽ ra kết quả 2 1 2
Như ở phần khuôn mẫu hàm thì “typename
” và “class
” là như nhau, nhưng trong trường hợp này thì lại phải dùng “typename
”
template <typename Type> class Point { Type p; };
Ở đây A
là 1 kiểu của Type
vì thế mà p
được hiểu là 1 con trỏ kiểu Type::A*
, khi đó nếu muốn khai báo thể hiện của template Point
thì làm như sau
class Q { public: typedef int A; }; Point<Q> z;
Còn nếu như bỏ “typename
”
template <class Type> class Point { Type::A * p; };
Lúc này Type::A * p
được hiểu là tích giữa biến static A
của lớp Type
với 1 biến p
nào đó. Ngoài ra đối với khuôn mẫu lớp có thể kế thừa giống như những các lớp thông thường
template <class Type> class Point { protected: Type x; Type y; };
template <class Type> class Point3D: public Point<Type> { Type z; };
Template cho phép tổng quát hóa kiểu dữ liệu và đồng thời hỗ trợ thao tác, xử lý chuyên môn trên 1 kiểu hay nhiều kiểu nhất định
template <class Type> class Point { public: Point() { printf("Not explicit specialization\n"); } }; template<> class Point<int> { public: Point() { printf(“Explicit specialization\n”); } };
Lợi ích của việc dùng template để tổng quát hóa kiểu dữ liệu
Việc sử dụng từ khóa template
làm giảm nhiều thời gian và công sức phải code lại 1 hàm dùng cho nhiều kiểu dữ liệu, dễ dàng bảo trì, phát triển hay thay đổi mã nguồn.
Khi biên dịch, các hàm và lớp sẽ được phát sinh đầy đủ theo nhu cầu của chương trình trước khi được gọi thực thi nên tốc độ của chương trình sẽ giống như cài đặt nhiều hàm hay lớp độc lập cho mỗi kiểu dữ liệu tương ứng.