Trong bài này chúng ta sẽ học template trong C++: cách viết một hàm hay một class dùng được cho mọi kiểu dữ liệu, trình biên dịch sinh code từ template thế nào, và những lỗi hay gặp khi dùng template. Template là nền móng của toàn bộ thư viện chuẩn: std::vector<int>, std::unique_ptr<Sach> mà các bạn dùng từ đầu serie đều là template.
Bài 24 trong Serie Lập trình C++ từ A tới Z
Vì sao cần template trong C++?
Viết hàm tìm số lớn hơn trong 2 số. Với int, với double, với std::string… cách làm giống hệt nhau, chỉ khác kiểu. Dùng nạp chồng hàm thì phải viết lại cùng một đoạn code cho từng kiểu, sửa một chỗ phải nhớ sửa mọi chỗ.
Template (khuôn mẫu) cho phép viết code một lần với kiểu dữ liệu là tham số, rồi trình biên dịch tự sinh ra phiên bản cho từng kiểu cần dùng. Giống như khuôn đúc bánh: một cái khuôn, đổ bột gì vào thì ra bánh nấy.
Function template
#include <iostream>
#include <string>
template <typename T>
T lonNhat(T a, T b)
{
return (a > b) ? a : b;
}
int main()
{
std::cout << lonNhat(3, 7) << '\n'; // T = int
std::cout << lonNhat(2.5, 1.5) << '\n'; // T = double
std::cout << lonNhat(std::string("Clean Code"), std::string("Lap trinh C++")) << '\n'; // T = std::string
std::cout << lonNhat<double>(3, 4.5) << '\n'; // chi ro T = double
return 0;
}
template <typename T>khai báoTlà một tham số kiểu: chưa biết là kiểu gì, sẽ được thay khi dùng.- Khi gọi
lonNhat(3, 7), trình biên dịch nhìn kiểu đối số làintnên tự suy raT = int. Gọi là suy luận kiểu (template argument deduction). - Muốn chỉ rõ kiểu thì viết trong ngoặc nhọn:
lonNhat<double>(3, 4.5).
Bên dưới, template không phải là một hàm. Mỗi kiểu được dùng, trình biên dịch sinh ra một hàm riêng:
Vì code được sinh ra lúc biên dịch, template chạy nhanh như hàm viết tay cho từng kiểu, khác với đa hình bằng hàm ảo phải tra bảng lúc chạy.
Lỗi suy luận kiểu
Hai đối số khác kiểu thì trình biên dịch không biết chọn T là gì:
#include <iostream>
template <typename T>
T lonNhat(T a, T b)
{
return (a > b) ? a : b;
}
int main()
{
std::cout << lonNhat(3, 4.5) << '\n'; // 3 la int, 4.5 la double
return 0;
}
3 gợi ý T = int, 4.5 gợi ý T = double, mâu thuẫn nên báo template argument deduction/substitution failed. Template không tự chuyển kiểu như hàm thường. Cách sửa: chỉ rõ lonNhat<double>(3, 4.5) như ví dụ đầu, hoặc truyền 3.0.
Kiểu không hỗ trợ thao tác trong template
Template dùng được với mọi kiểu, miễn là kiểu đó hỗ trợ các thao tác mà template dùng tới. lonNhat dùng toán tử >, nên kiểu nào không có > thì không dùng được:
#include <iostream>
struct Sach
{
int ma;
};
template <typename T>
T lonNhat(T a, T b)
{
return (a > b) ? a : b;
}
int main()
{
Sach a{1}, b{2};
lonNhat(a, b); // Sach khong co toan tu >
return 0;
}
Lỗi được báo bên trong template (no match for 'operator>'), kèm dòng required from here chỉ về chỗ gọi trong main. Thông báo lỗi template thường dài, mẹo đọc là tìm dòng required from here để biết lời gọi nào gây lỗi. Sửa bằng cách thêm operator> cho Sach như bài nạp chồng toán tử.
Class template
Template dùng được cho cả class. Ví dụ một kho chứa dùng chung cho sách, mã thẻ hay bất kỳ thứ gì:
#include <iostream>
#include <string>
#include <vector>
template <typename T>
class Kho
{
public:
void them(const T& x) { ds_.push_back(x); }
std::size_t soLuong() const { return ds_.size(); }
const T& layRa(std::size_t viTri) const { return ds_.at(viTri); }
void inTatCa() const
{
for (const T& x : ds_)
std::cout << " " << x << '\n';
}
private:
std::vector<T> ds_;
};
int main()
{
Kho<std::string> khoSach;
khoSach.them("Lap trinh C++");
khoSach.them("Clean Code");
Kho<int> khoMaThe;
khoMaThe.them(1024);
khoMaThe.them(2048);
khoMaThe.them(4096);
std::cout << "Kho sach (" << khoSach.soLuong() << "):\n";
khoSach.inTatCa();
std::cout << "Kho ma the (" << khoMaThe.soLuong() << "):\n";
khoMaThe.inTatCa();
return 0;
}
Với class template, các bạn phải ghi kiểu trong ngoặc nhọn khi tạo đối tượng: Kho<std::string>, Kho<int>. Đây chính là cách viết std::vector<int> quen thuộc. Kho<int> và Kho<std::string> là 2 class hoàn toàn khác nhau do trình biên dịch sinh ra.
Tham số hằng trong template
Tham số template không nhất thiết là kiểu, có thể là một hằng số biết lúc biên dịch. std::array<int, 5> dùng đúng cách này:
#include <iostream>
#include <string>
template <typename T, std::size_t N>
class KeCoDinh
{
public:
bool dat(const T& x)
{
if (dem_ == N)
return false;
ngan_[dem_++] = x;
return true;
}
std::size_t sucChua() const { return N; }
std::size_t dangChua() const { return dem_; }
private:
T ngan_[N];
std::size_t dem_ = 0;
};
int main()
{
KeCoDinh<std::string, 2> keNho;
std::cout << "Dat cuon 1: " << keNho.dat("Lap trinh C++") << '\n';
std::cout << "Dat cuon 2: " << keNho.dat("Clean Code") << '\n';
std::cout << "Dat cuon 3: " << keNho.dat("Python co ban") << '\n';
std::cout << "Ke chua " << keNho.dangChua() << "/" << keNho.sucChua() << " cuon\n";
std::cout << "sizeof(KeCoDinh<int, 100>) = " << sizeof(KeCoDinh<int, 100>) << " byte\n";
return 0;
}
N là số ngăn của kệ, cố định lúc biên dịch. Vì vậy mảng ngan_[N] nằm ngay trong đối tượng, không cần cấp phát trên heap: KeCoDinh<int, 100> có kích thước 408 byte, tức 100 số int cộng biến đếm. Kiểu “kích thước cố định, không dùng heap” này rất được ưa chuộng trong lập trình nhúng.
Template phải viết trong file header
Với hàm và class thường, các bạn khai báo trong .h và định nghĩa trong .cpp như bài về hàm. Làm vậy với template thì:
// kho.h
#ifndef KHO_H
#define KHO_H
#include <vector>
template <typename T>
class Kho
{
public:
void them(const T& x);
std::size_t soLuong() const;
private:
std::vector<T> ds_;
};
#endif
// kho.cpp
#include "kho.h"
template <typename T>
void Kho<T>::them(const T& x) { ds_.push_back(x); }
template <typename T>
std::size_t Kho<T>::soLuong() const { return ds_.size(); }
Lỗi liên kết undefined reference to 'Kho<int>::them(int const&)'. Lý do: trình biên dịch chỉ sinh code cho Kho<int> ở nơi nhìn thấy cả lời gọi lẫn phần định nghĩa template. Khi biên dịch kho.cpp, nó thấy định nghĩa nhưng không ai dùng Kho<int>, nên không sinh gì. Khi biên dịch main.cpp, nó thấy lời gọi nhưng không thấy định nghĩa. Kết quả là không file .o nào có Kho<int>::them.
Lưu ý: Template phải viết toàn bộ phần định nghĩa trong file header .h, không tách ra .cpp. Đó là lý do mở các header thư viện chuẩn như <vector> sẽ thấy toàn bộ code nằm ở đó.
Khi nào nên dùng template?
- Khi cùng một thuật toán hay cấu trúc dữ liệu cần dùng cho nhiều kiểu: hàm tìm lớn nhất, sắp xếp, danh sách, kho chứa.
- Khi cần hiệu năng: template giải quyết lúc biên dịch, không tốn chi phí lúc chạy như hàm ảo.
- Không nên dùng khi chỉ có 1–2 kiểu thật sự cần: template làm code khó đọc hơn, thông báo lỗi dài hơn, và thời gian biên dịch lâu hơn.
Phần lớn thời gian, các bạn dùng template có sẵn trong thư viện chuẩn nhiều hơn là tự viết. Bài tiếp theo về STL là ví dụ rõ nhất.
Bài tập
- Viết function template
void hoanDoi(T& a, T& b)và dùng vớiint,double,std::string. - Viết function template
T tongMang(const std::vector<T>& v)tính tổng các phần tử. Thử vớistd::vector<std::string>, kết quả là gì? - Thêm phương thức
bool chua(const T& x) constvào classKhođể kiểm tra một phần tử có trong kho không. KiểuTcần hỗ trợ toán tử nào? - Viết class template
Cap<A, B>lưu một cặp giá trị 2 kiểu khác nhau, ví dụCap<int, std::string>cho mã sách và tên sách.
Lời giải sẽ có trong bài tập tổng hợp của chương nhé.
Kết
Template trong C++ cho phép viết hàm và class một lần, dùng cho mọi kiểu dữ liệu, và trình biên dịch sinh code riêng cho từng kiểu lúc biên dịch nên không tốn chi phí lúc chạy. Ba điều cần nhớ: đối số phải cùng kiểu để suy luận được T, kiểu được dùng phải hỗ trợ mọi thao tác template cần, và template phải viết trọn trong file header.
Tham khảo thêm ở cppreference: templates. Bài tiếp theo chúng ta sẽ dùng các template có sẵn trong thư viện chuẩn STL: map, set và các thuật toán. Thấy bài viết có ích thì chia sẻ cho bạn bè nhé.








