Menu

Template trong C++: giải thích hàm và lớp generic

Viết code một lần và dùng được cho mọi kiểu với template trong C++: function template, class template, suy luận kiểu và những lỗi biên dịch khó hiểu mà chúng gây ra.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Viết một lần, dùng cho mọi kiểu

Ở trang trước, bạn đã sắp xếp một vector<int> bằng std::sort. Nhưng std::sort cũng sắp xếp được vector<string>, vector<double> hay một mảng struct do bạn tự định nghĩa, mà không ai phải viết riêng một hàm sort cho từng kiểu. Đó không phải phép màu, cũng không phải nạp chồng hàm. Đó là template: một đoạn code duy nhất mà trình biên dịch dùng lại cho bất kỳ kiểu nào bạn đưa vào.

Không có template, bạn sẽ phải sao chép cùng một logic cho từng kiểu. Dưới đây là cùng một hàm maximum được viết ba lần, đúng kiểu lặp lại mà template sinh ra để loại bỏ:

int    maximum(int a, int b)       { return a > b ? a : b; }
double maximum(double a, double b) { return a > b ? a : b; }
string maximum(string a, string b) { return a > b ? a : b; }

Thân hàm giống hệt nhau. Chỉ có kiểu là khác. Template cho phép bạn nói "hàm này dùng được cho mọi kiểu T" và chỉ viết một lần.

Function template

Bạn biến một hàm thành template bằng cách thêm template <typename T> phía trước và dùng T ở mọi chỗ mà bình thường bạn sẽ ghi một kiểu cụ thể.

Để ý rằng bạn chưa hề viết maximum<int> hay maximum<double>. Trình biên dịch nhìn vào các đối số và tự xác định T nên là gì, việc này gọi là suy luận đối số template (template argument deduction). Mỗi kiểu khác nhau mà bạn gọi hàm sẽ khiến trình biên dịch khởi tạo (tạo ra) một hàm cụ thể riêng ở phía sau.

Bạn có thể ghi rõ kiểu bằng dấu ngoặc nhọn khi việc suy luận không giúp được:

Có một cái bẫy hay gặp khi suy luận kiểu. Vì T phải là một kiểu duy nhất, trộn lẫn các kiểu đối số sẽ làm nó hỏng:

maximum(3, 7.5);   // ERROR: is T int or double? The compiler refuses to guess.

Bạn có thể sửa bằng cách ghi rõ kiểu, maximum<double>(3, 7.5), hoặc cho mỗi tham số một tham số kiểu riêng, như phần tiếp theo.

Nhiều tham số kiểu

Template không bị giới hạn ở một kiểu. Liệt kê bao nhiêu tùy nhu cầu, phân cách bằng dấu phẩy. Đây là cách viết một hàm có các tham số thuộc những kiểu khác nhau:

Khi kiểu trả về phụ thuộc vào các tham số, hãy để trình biên dịch tự tính bằng auto (từ C++14 trở đi), thứ kết hợp rất tự nhiên với template:

Class template

Template không chỉ dành cho hàm: cả một lớp cũng có thể là template. Các container chuẩn hoạt động đúng như vậy: vector<int>, map khóa và giá trị và pair<A, B> đều là class template. Bạn viết cấu trúc dữ liệu một lần và nó lưu được bất kỳ kiểu nào bạn truyền vào làm tham số.

Đây là một lớp Box generic nhỏ, chứa một giá trị thuộc bất kỳ kiểu nào:

Khác biệt chính so với function template: với class template, bạn thường phải cung cấp kiểu trong dấu ngoặc nhọn, Box<int>, vì ở các chuẩn cũ không có đối số hàm khởi tạo nào để suy luận. (C++17 đã bổ sung class template argument deduction, nên Box b(42); cũng chạy được, nhưng ghi rõ kiểu luôn an toàn và dễ đọc.)

Lỗi sẽ rất dài, và đây là lý do

Đây là phần khiến ai cũng vấp, nên cần nói thẳng. Template chỉ được kiểm tra đầy đủ khi được khởi tạo với một kiểu thật. Bạn có thể viết một template dùng < và nó tự biên dịch bình thường; lỗi chỉ xuất hiện đúng lúc bạn khởi tạo nó với một kiểu không có <.

template <typename T>
T maximum(T a, T b) {
    return a > b ? a : b;   // requires that T supports >
}

struct Point { int x, y; };

// maximum(Point{1,2}, Point{3,4});
// ERROR: no operator > for Point. The message names Point AND
// quotes this whole function, often spanning many lines.

Vì trình biên dịch thay kiểu đầy đủ vào template và báo lỗi từ bên trong code được tạo ra, chỉ một sai sót có thể sinh ra cả một bức tường thông báo nhắc đến chi tiết nội bộ của thư viện. Hai mẹo để sống sót:

  • Đọc lỗi đầu tiên, không phải lỗi cuối cùng. Các lỗi sau thường chỉ là hệ quả của lỗi đầu.
  • Tìm tên kiểu của chính bạn trong thông báo (ở đây là Point). Nó cho biết lần khởi tạo nào bị hỏng.

Cách sửa thật sự là đảm bảo kiểu của bạn hỗ trợ mọi thứ template cần: với maximum, nghĩa là nạp chồng operator> cho Point, một chủ đề của trang sau. Concepts trong C++20 hiện đại có thể đưa các lỗi này lên sớm hơn và dễ đọc hơn, nhưng cơ chế thay thế kiểu bên dưới vẫn như cũ.

Tiếp theo: lớp

Bạn vừa xây dựng class template Box, một lớp có dữ liệu private, hàm khởi tạo và hàm thành viên, trong lúc tập trung vào template. Trang tiếp theo sẽ đi chậm lại và dạy về lớp (class) một cách bài bản: cách gom dữ liệu với các hàm thao tác trên nó, public và private thực sự kiểm soát điều gì, và hàm thành viên truy cập trạng thái của đối tượng ra sao. Template và lớp luôn đi cùng nhau trong C++ thực tế, nên nắm vững lớp sẽ giúp viết code generic dễ hơn nhiều.

Câu hỏi thường gặp

Template trong C++ là gì?

Template là một bản thiết kế cho phép bạn viết một hàm hoặc một lớp một lần, rồi để trình biên dịch tạo ra một phiên bản cho từng kiểu bạn dùng. Bạn viết template <typename T> rồi dùng T thay cho kiểu thật. Trình biên dịch sẽ tạo ra một phiên bản cụ thể, việc này gọi là instantiation (khởi tạo template).

typename và class trong template C++ khác nhau thế nào?

Trong danh sách tham số template, template <typename T> và template <class T> có nghĩa hoàn toàn giống nhau. Ngày nay typename thường được ưa chuộng hơn vì nó phản ánh đúng bản chất: T có thể là bất kỳ kiểu nào, không chỉ là class. Việc chọn từ khóa nào không ảnh hưởng gì đến code được tạo ra.

Vì sao thông báo lỗi template trong C++ dài đến vậy?

Template được kiểm tra khi được khởi tạo với một kiểu thật, chứ không phải lúc bạn viết nó. Nếu một kiểu không hỗ trợ thao tác bạn dùng (như < khi sắp xếp), lỗi sẽ xuất hiện sâu bên trong code thư viện, kèm theo tên kiểu đầy đủ đã được khởi tạo, tạo ra hàng trang đầu ra. Hãy đọc lỗi đầu tiên và tìm tên kiểu của bạn trong đó.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu