Nội dung
Khuôn mẫu (Template) là cơ sở để lập trình tổng quát trong C++. Là một ngôn ngữ nghiêm ngặt về kiểu, C++ yêu cầu tất cả các biến phải có một kiểu cụ thể, được lập trình viên khai báo rõ ràng hoặc suy ra bởi trình biên dịch. Tuy nhiên, nhiều cấu trúc dữ liệu và thuật toán trông giống nhau cho dù chúng xử lý trên kiểu nào. Mẫu cho phép chúng ta xác định các hoạt động của một lớp hoặc hàm và cho phép ta chỉ định kiểu cụ thể mà các toán hạng đó nên hoạt động.
Định nghĩa và sử dụng mẫu
Khuôn mẫu là một cấu trúc tạo ra một kiểu hoặc hàm thông thường tại thời điểm biên dịch dựa trên các đối số mà ta cung cấp cho các đối số mẫu. Ví dụ ta có thể định nghĩa một mẫu hàm như thế này:
template T minimum(const T& lhs, const T& rhs)
{
return lhs < rhs ? lhs : rhs;
}
Đoạn code trên mô tả một mẫu cho một hàm tổng quát có duy nhất tham số kiểu T, có giá trị trả về và đối số gọi hàm (lhs và rhs) đều thuộc kiểu này. Ta có thể đặt tên cho một đối số kiểu là bất cứ thứ gì bạn thích, nhưng theo quy ước, các chữ cái in hoa thường được sử dụng phổ biến nhất. T là một đối số mẫu; từ khóa typename cho biết rằng đối số này là đại diện cho một kiểu nào đó. Khi hàm được gọi, trình biên dịch sẽ thay thế mọi phiên bản bằng đối số kiểu cụ thể được chỉ định bởi chúng ta hoặc được suy ra bởi trình biên dịch. Quá trình mà trong đó trình biên dịch tạo ra một lớp hoặc hàm từ một mẫu được gọi là khởi tạo mẫu.
Ở những chổ còn lại, ta có thể khai báo một thể hiện của mẫu với kiểu chỉ định int. Giả sử rằng get_a() và get_b() là các hàm trả về kiểu int:
int a = get_a();
int b = get_b();
int i = minimum<int>(a, b);
Tuy nhiên, vì đây là mẫu hàm và trình biên dịch có thể suy ra kiểu từ các đối số a và b, nên ta có thể gọi nó giống như một hàm thông thường:
int i = minimum(a, b);
Khi trình biên dịch gặp câu lệnh này, nó sẽ tạo ra một hàm mới trong đó mọi lần xuất hiện của T trong mẫu được thay thế bằng int:
int minimum(const int& lhs, const int& rhs)
{
return lhs < rhs ? lhs : rhs;
}
Tham số kiểu
Trong mẫu ở trên, lưu ý rằng đối số kiểu T không được định tính theo bất kỳ cách nào cho đến khi nó được sử dụng trong các đối số gọi hàm, mà trong đó các đối số const và các tham chiếu được thêm vào.
Không có giới hạn thực tế cho số lượng đối số kiểu. Phân tách nhiều đối số bằng dấu phẩy:
template <typename T, typename U, typename V> class Foo{};
Từ khóa class tương đương với typename trong ngữ cảnh này. Do đó, ta có thể viết:
template <class T, class U, class V> class Foo{};
Bạn cũng có thể sử dụng toán tử dấu chấm lửng (…) để xác định một mẫu có số lượng đối số kiểu là 0 hoặc nhiều tùy ý:
template<typename... Arguments> class vtclass;
vtclass< > vtinstance1;
vtclass<int> vtinstance2;
vtclass<float, bool> vtinstance3;
Bất kỳ kiểu dựng sẵn hoặc người dùng định nghĩa đều có thể được sử dụng làm đối số kiểu. Chẳng hạn, ta có thể sử dụng std::vector trong gói thư viện chuẩn (STL) để lưu trữ các biến kiểu int, double, std::string, MyClass, const MyClass *, MyClass,… Hạn chế chính khi sử dụng các mẫu là một tham số kiểu phải hỗ trợ các toán hạng mà áp dụng cho các đối số kiểu. Ví dụ, nếu chúng ta gọi hàm minimum cho đối số kiểu MyClass như sau:
class MyClass
{
public:
int num;
std::wstring description;
};
int main()
{
MyClass mc1 {1, L"hello"};
MyClass mc2 {2, L"goodbye"};
auto result = minimum(mc1, mc2); // Error! C2678
}
Trình biên dịch sẽ báo lỗi do toán tử < không được override trong lớp MyClass. Không có yêu cầu cố hữu rằng tất cả các tham số kiểu cho bất kỳ mẫu cụ thể nào đều phải thuộc cùng một cấu trúc đối tượng, mặc dù bạn có thể tạo một mẫu mà tuân theo hạn chế đó. Bạn có thể kết hợp các kỹ thuật hướng đối tượng với các mẫu; ví dụ, bạn có thể lưu trữ Derived* trong một vector<Base*> . Lưu ý rằng các đối số phải là con trỏ
vector<MyClass*> vec;
MyDerived d(3, L"back again", time(0));
vec.push_back(&d);
// or more realistically:
vector<shared_ptr<MyClass>> vec2;
vec2.push_back(make_shared<MyDerived>());
Các yêu cầu cơ bản mà std::vector và thư viện container tiêu chuẩn khác áp đặt cho các phần tử của T là T có thể được sao chép thông qua phép gán hoặc hàm xây dựng.
Các tham số không có kiểu
mmmKhông giống như các kiểu tổng quát trong các ngôn ngữ khác như C# và Java, mẫu trong C++ hỗ trợ các tham số không có kiểu, còn được gọi là tham số giá trị. Bạn có thể cung cấp một giá trị nguyên hằng để chỉ định độ dài của một mảng như với ví dụ sau tương tự như lớp std::aray trong thư viện chuẩn,
template<typename T, size_t L>
class MyArray
{
T arr[L];
public:
MyArray() { ... }
};
Lưu ý cú pháp trong khai báo mẫu. Giá trị size_t được truyền vào dưới dạng đối số mẫu tại thời gian biên dịch và phải là const hoặc biểu thức constexpr. Bạn sử dụng nó như thế sau,
MyArray<MyClass*, 10> arr;
Các kiểu giá trị khác bao gồm con trỏ và tham chiếu có thể được truyền vào dưới dạng tham số không kiểu. Chẳng hạn, ta có thể chuyển một con trỏ tới một hàm hoặc đối tượng hàm để tùy chỉnh một số toán hạng bên trong mẫu.
Suy đoán kiểu cho các tham số không kiểu trong mẫu,
Trong C++17, trình biên dịch có thể tự suy diễn ra kiểu của đối số không kiểu khi có từ khóa auto.
template <auto x> constexpr auto constant = x;
auto v1 = constant<5>; // v1 == 5, decltype(v1) is int
auto v2 = constant<true>; // v2 == true, decltype(v2) is bool
auto v3 = constant<'a'>; // v3 == 'a', decltype(v3) is char
Sử dụng mẫu như là tham số mẫu
Một mẫu có thể là một tham số mẫu. Trong ví dụ sau, MyClass2 có hai tham số là tham số kiểu T và tham số mẫu Arr,
template<typename T, template<typename U, int I> class Arr>
class MyClass2
{
T t; //OK
Arr<T, 10> a;
U u; //Error. U not in scope
};
Vì bản thân tham số Arr không có phần thân, nên ta không cần tên tham số của nó. Trong thực tế, đó là lỗi khi tham chiếu tên kiểu chữ hoặc tên tham số lớp của Arr từ bên trong phần thân của MyClass2. Vì lý do này, tên tham số kiểu của Arr có thể được bỏ qua, như sau,
template<typename T, template<typename, int> class Arr>
class MyClass2
{
T t; //OK
Arr<T, 10> a;
};
Tham số mẫu mặc định
Các mẫu lớp và hàm có thể có các đối số mặc định. Khi một mẫu có một đối số mặc định, ta có thể bỏ qua khi sử dụng nó. Ví dụ, mẫu std::vector có một đối số mặc định cho bộ cấp phát,
template <class T, class Allocator = allocator<T>> class vector;
Trong hầu hết các trường hợp, ta sử dụng std::allocator mặc định, vì vậy một vector thường được khai báo như thế này,
vector<int> myInts;
Nhưng nếu cần, ta có thể chỉ định một bộ cấp phát tùy chỉnh như sau,
vector<int, MyAllocator> ints;
Trong trường hợp có nhiều đối số mẫu, tất cả các đối số sau đối số mặc định đầu tiên đều phải có đối số mặc định.
Khi sử dụng một mẫu có các tham số mặc định, ta sử dụng dấu ngoặc rỗng,
template<typename A = int, typename B = double>
class Bar
{
//...
};
...
int main()
{
Bar<> bar; // use all default type arguments
}
Khuôn mẫu đặc biệt
Trong một số trường hợp, một mẫu không thể định nghĩa chính xác cùng một chương trình cho bất kỳ kiểu nào. Giả sử, ta muốn xác định đoạn chương trình mà chỉ được thực thi nếu đối số kiểu là con trỏ hoặc chuỗi std::wstring hoặc các kiểu được dẫn xuất từ một lớp cơ sở cụ thể. Trong những trường hợp như vậy, ta có thể định nghĩa một mẫu đặc biệt cho kiểu cụ thể đó. Khi khởi tạo mẫu với kiểu đó, trình biên dịch sẽ sử dụng mẫu đặc biệt để tạo lớp và đối với tất cả các kiểu khác, trình biên dịch sẽ chọn mẫu tổng quát hơn. Việc đặc biệt hóa mà trong đó tất cả các tham số đều đặc biệt là Đặc biệt hóa đầy đủ. Nếu chỉ một số tham số đặc biệt, nó được gọi là đặc biệt hóa một phần.
template <typename K, typename V>
class MyMap{/*...*/};
// partial specialization for string keys
template<typename V>
class MyMap<string, V> {/*...*/};
...
MyMap<int, MyClass> classes; // uses original template
MyMap<string, MyClass> classes2; // uses the partial specialization
Một mẫu có thể có bất kỳ số lượng đặc biệt hóa nào miễn là mỗi tham số kiểu đặc biệt là duy nhất. Chỉ các mẫu lớp (class template) có thể có mẫu đặc biệt hóa một phần. Tất cả các mẫu đặc biệt hóa đầy đủ và một phần của một mẫu phải được khai báo trong cùng một không gian tên (namespace) như mẫu gốc. Để biết thêm nhiều thông tin hơn, hãy xem thêm bài viết về Sự đặc biệt hóa mẫu trong chuỗi series về Template trong C++.
Để lại một bình luận