Search…

template Trong C++

17/09/20209 min read
Tìm hiểu về từ khoá template trong C++.

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ằng typename, ở đâ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 Typeint 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.

IO Stream

IO Stream Co., Ltd

30 Trinh Dinh Thao, Hoa Thanh ward, Tan Phu district, Ho Chi Minh city, Vietnam
+84 28 22 00 11 12
developer@iostream.co

383/1 Quang Trung, ward 10, Go Vap district, Ho Chi Minh city
Business license number: 0311563559 issued by the Department of Planning and Investment of Ho Chi Minh City on February 23, 2012

©IO Stream, 2013 - 2024