Trong hai bài học trước, bạn tìm hiểu cách Các mẫu hàm, và ví dụ, cho phép chúng ta khái quát các hàm để làm việc với nhiều kiểu dữ liệu khác nhau. Mặc dù đây là một khởi đầu tuyệt vời cho con đường lập trình tổng quát, nhưng nó không giải quyết được tất cả các vấn đề của chúng ta. Chúng ta hãy xem một ví dụ về một vấn đề như vậy và xem những mẫu nào có thể giúp thêm cho chúng ta.

1. Mẫu và các lớp container

Bạn đã học cách sử dụng thành phần để triển khai các lớp có chứa nhiều phiên bản của các lớp khác. Như một ví dụ về một container, chúng ta đã xem xét lớp IntArray. Đây là một ví dụ đơn giản về lớp đó:

/**
* Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
*
* @author cafedevn
* Contact: cafedevn@gmail.com
* Fanpage: https://www.facebook.com/cafedevn
* Instagram: https://instagram.com/cafedevn
* Twitter: https://twitter.com/CafedeVn
* Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/

#ifndef INTARRAY_H
#define INTARRAY_H
 
#include <cassert>
 
class IntArray
{
private:
    int m_length{};
    int *m_data{};
 
public:
 
    IntArray(int length)
    {
        assert(length > 0);
        m_data = new int[length]{};
        m_length = length;
    }
 
    // We don't want to allow copies of IntArray to be created.
    IntArray(const IntArray&) = delete;
    IntArray& operator=(const IntArray&) = delete;
 
    ~IntArray()
    {
        delete[] m_data;
    }
 
    void Erase()
    {
        delete[] m_data;
        // We need to make sure we set m_data to 0 here, otherwise it will
        // be left pointing at deallocated memory!
        m_data = nullptr;
        m_length = 0;
    }
 
    int& operator[](int index)
    {
        assert(index >= 0 && index < m_length);
        return m_data[index];
    }
 
    int getLength() const { return m_length; }
};
 
#endif

Trong khi lớp này cung cấp một cách dễ dàng để tạo các mảng số nguyên, nếu chúng ta muốn tạo một mảng double thì sao? Sử dụng các phương pháp lập trình truyền thống, chúng ta phải tạo ra một lớp hoàn toàn mới! Đây là một ví dụ về DoubleArray, một lớp mảng được sử dụng để giữ số double.

/**
* Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
*
* @author cafedevn
* Contact: cafedevn@gmail.com
* Fanpage: https://www.facebook.com/cafedevn
* Instagram: https://instagram.com/cafedevn
* Twitter: https://twitter.com/CafedeVn
* Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/

#ifndef DOUBLEARRAY_H
#define DOUBLEARRAY_H
 
#include <cassert>
 
class DoubleArray
{
private:
    int m_length{};
    double *m_data{};
 
public:
 
    DoubleArray(int length)
    {
        assert(length > 0);
        m_data = new double[length]{};
        m_length = length;
    }
 
    DoubleArray(const DoubleArray&) = delete;
    DoubleArray& operator=(const DoubleArray&) = delete;
 
    ~DoubleArray()
    {
        delete[] m_data;
    }
 
    void Erase()
    {
        delete[] m_data;
        // We need to make sure we set m_data to 0 here, otherwise it will
        // be left pointing at deallocated memory!
        m_data = nullptr;
        m_length = 0;
    }
 
    double& operator[](int index)
    {
        assert(index >= 0 && index < m_length);
        return m_data[index];
    }
 
    int getLength() const { return m_length; }
};
 
#endif

Mặc dù danh sách code dài, nhưng bạn sẽ lưu ý hai lớp gần như giống hệt nhau! Trong thực tế, sự khác biệt đáng kể duy nhất là kiểu dữ liệu được chứa (int so với double). Như bạn có thể đoán, đây là một lĩnh vực khác mà các mẫu có thể được sử dụng tốt, để giải phóng chúng ta khỏi việc phải tạo các lớp bị ràng buộc với một kiểu dữ liệu cụ thể.

Tạo các lớp mẫu hoạt động khá giống nhau để tạo các hàm mẫu, vì vậy chúng ta sẽ tiến hành bằng ví dụ. Đây là lớp mảng của chúng ta, phiên bản templated:

Array.h:

#ifndef ARRAY_H
#define ARRAY_H
 
#include <cassert>
 
template <class T>
class Array
{
private:
    int m_length{};
    T *m_data{};
 
public:
 
    Array(int length)
    {
        assert(length > 0);
        m_data = new T[length]{};
        m_length = length;
    }
 
    Array(const Array&) = delete;
    Array& operator=(const Array&) = delete;
 
    ~Array()
    {
        delete[] m_data;
    }
 
    void Erase()
    {
        delete[] m_data;
        // We need to make sure we set m_data to 0 here, otherwise it will
        // be left pointing at deallocated memory!
        m_data = nullptr;
        m_length = 0;
    }
 
    T& operator[](int index)
    {
        assert(index >= 0 && index < m_length);
        return m_data[index];
    }
 
    // templated getLength() function defined below
    int getLength() const; 
};
 
// member functions defined outside the class need their own template declaration
template <class T>
int Array<T>::getLength() const // note class name is Array<T>, not Array
{
  return m_length;
}
 
#endif

Như bạn có thể thấy, phiên bản này gần giống với phiên bản IntArray, ngoại trừ chúng ta đã thêm khai báo mẫu và thay đổi kiểu dữ liệu được chứa từ int thành T.

Lưu ý rằng chúng ta cũng đã định nghĩa hàm getLength() bên ngoài khai báo lớp. Điều này là không cần thiết, nhưng các lập trình viên mới thường vấp ngã khi cố gắng làm điều này lần đầu tiên do cú pháp, vì vậy một ví dụ mang tính hướng dẫn. Mỗi hàm thành viên templated được khai báo bên ngoài khai báo lớp cần khai báo mẫu riêng. Ngoài ra, lưu ý rằng tên của lớp Array templated là Array <T>, không phải Array – Array sẽ đề cập đến một phiên bản không có templated của một lớp có tên Array.

Dưới đây là một ví dụ ngắn sử dụng lớp mảng templated ở trên:

#include <iostream>
#include "Array.h"
 
int main()
{
	Array<int> intArray(12);
	Array<double> doubleArray(12);
 
	for (int count{ 0 }; count < intArray.getLength(); ++count)
	{
		intArray[count] = count;
		doubleArray[count] = count + 0.5;
	}
 
	for (int count{ intArray.getLength() - 1 }; count >= 0; --count)
		std::cout << intArray[count] << '\t' << doubleArray[count] << '\n';
 
	return 0;
}

Ví dụ này in như sau:

11     11.5
10     10.5
9       9.5
8       8.5
7       7.5
6       6.5
5       5.5
4       4.5
3       3.5
2       2.5
1       1.5
0       0.5

Các lớp mẫu được kích hoạt theo cùng một cách các hàm mẫu – trình biên dịch sẽ sao chép một bản sao theo yêu cầu, với tham số mẫu được thay thế bằng kiểu dữ liệu thực tế mà người dùng cần, sau đó biên dịch bản sao. Nếu bạn chưa từng sử dụng một lớp mẫu, trình biên dịch thậm chí sẽ không biên dịch nó.

Các lớp mẫu là lý tưởng để thực hiện các lớp container, bởi vì rất mong muốn các container hoạt động trên nhiều kiểu dữ liệu khác nhau và các mẫu cho phép bạn làm như vậy mà không cần sao chép code. Mặc dù cú pháp là xấu và các thông báo lỗi có thể khó hiểu, các lớp mẫu thực sự là một trong những tính năng hữu ích và tốt nhất của C ++.

2. Các lớp mẫu trong thư viện chuẩn

Bây giờ chúng ta đã bao phủ các lớp mẫu, bạn sẽ hiểu std :: vector <int> nghĩa là gì bây giờ – std :: vector thực sự là một lớp mẫu và int là tham số kiểu cho mẫu! Thư viện chuẩn có đầy đủ các lớp mẫu được xác định trước có sẵn để bạn sử dụng. Chúng ta sẽ đề cập đến những điều này trong các chương sau.

3. Tách các lớp mẫu

Một khuôn mẫu không phải là một lớp hoặc một hàm – nó là một mẫu được sử dụng để tạo các lớp hoặc các hàm. Như vậy, nó không hoạt động theo cách tương tự như các hàm hoặc lớp thông thường. Trong hầu hết các trường hợp, đây không phải là một vấn đề. Tuy nhiên, có một lĩnh vực thường gây ra vấn đề cho các developer.

Với các lớp không phải khuôn mẫu, quy trình chung là đặt định nghĩa lớp trong file header và định nghĩa hàm thành viên trong file code .cpp có tên tương tự. Theo cách này, nguồn cho lớp được biên dịch thành một file dự án riêng biệt. Tuy nhiên, với các mẫu, điều này không hoạt động. Hãy xem xét những điều sau đây:

Array.h:

#ifndef ARRAY_H
#define ARRAY_H
 
#include <cassert>
 
template <class T>
class Array
{
private:
    int m_length{};
    T* m_data{};
 
public:
 
    Array(int length)
    {
        assert(length > 0);
        m_data = new T[length]{};
        m_length = length;
    }
 
    Array(const Array&) = delete;
    Array& operator=(const Array&) = delete;
 
    ~Array()
    {
        delete[] m_data;
    }
 
    void Erase()
    {
        delete[] m_data;
 
        m_data = nullptr;
        m_length = 0;
    }
 
    T& operator[](int index)
    {
        assert(index >= 0 && index < m_length);
        return m_data[index];
    }
 
    int getLength() const; 
};
 
#endif

Array.cpp:

/**
* Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
*
* @author cafedevn
* Contact: cafedevn@gmail.com
* Fanpage: https://www.facebook.com/cafedevn
* Instagram: https://instagram.com/cafedevn
* Twitter: https://twitter.com/CafedeVn
* Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/

#include "Array.h"
 
template <class T>
int Array<T>::getLength() const // note class name is Array<T>, not Array
{
  return m_length;
}

main.cpp:

#include "Array.h"
 
int main()
{
	Array<int> intArray(12);
	Array<double> doubleArray(12);
 
	for (int count{ 0 }; count < intArray.getLength(); ++count)
	{
		intArray[count] = count;
		doubleArray[count] = count + 0.5;
	}
 
	for (int count{ intArray.getLength() - 1 }; count >= 0; --count)
		std::cout << intArray[count] << '\t' << doubleArray[count] << '\n';
 
	return 0;
}

Chương trình trên sẽ biên dịch, nhưng gây ra lỗi liên kết:

unresolved external symbol "public: int __thiscall Array::getLength(void)" (?GetLength@?$Array@H@@QAEHXZ)

Để trình biên dịch sử dụng một mẫu, nó phải xem cả định nghĩa mẫu (không chỉ là khai báo) và kiểu mẫu được sử dụng để khởi tạo mẫu. Cũng nhớ rằng C ++ biên dịch các file riêng lẻ. Khi header Array.h được #included in main, định nghĩa lớp mẫu được sao chép vào main.cpp. Khi trình biên dịch thấy rằng chúng ta cần hai cá thể mẫu, Array <int> và Array <double>, nó sẽ khởi tạo chúng và biên dịch chúng như một phần của main.cpp. Tuy nhiên, khi nó được biên dịch riêng biệt Array.cpp, chúng ta sẽ quên rằng chúng ta cần một Array <int> và Array <double>, để hàm khuôn mẫu không bao giờ được khởi tạo. Do đó, chúng ta gặp lỗi liên kết, vì trình biên dịch không thể tìm thấy định nghĩa cho Array::getLength() or Array::getLength().

Có khá nhiều cách để giải quyết vấn đề này.

Cách đơn giản nhất là chỉ cần đặt tất cả code lớp mẫu của bạn vào file header (trong trường hợp này, đặt nội dung của Array.cpp vào Array.h, bên dưới lớp). Theo cách này, khi bạn #include header, tất cả code mẫu sẽ ở một nơi. Mặt trái của giải pháp này là nó đơn giản. Nhược điểm ở đây là nếu lớp mẫu được sử dụng ở nhiều nơi, bạn sẽ kết thúc với nhiều bản sao cục bộ của lớp mẫu, có thể tăng thời gian biên dịch và liên kết của bạn (trình liên kết của bạn nên xóa các định nghĩa trùng lặp, vì vậy không nên phình to thực thi của bạn). Đây là giải pháp ưa thích của chúng ta trừ khi thời gian biên dịch hoặc liên kết bắt đầu trở thành một vấn đề.

Nếu bạn cảm thấy việc đặt code Array.cpp vào header Array.h làm cho tiêu đề quá dài / lộn xộn, một cách khác là đổi tên Array.cpp thành Array.inl (.inl là viết tắt của dòng), sau đó include Array.inl từ dưới cùng của Array.h header. Điều đó mang lại kết quả tương tự như việc đặt tất cả code vào tiêu đề, nhưng giúp mọi thứ sạch sẽ hơn một chút.

Các giải pháp khác liên quan đến #including các file .cpp, nhưng chúng tôi không khuyến nghị những điều này vì cách sử dụng #include không chuẩn.

Một cách khác là sử dụng cách tiếp cận ba tệp. Định nghĩa lớp mẫu đi trong header. Các hàm thành viên lớp mẫu đi trong file code. Sau đó, bạn thêm một tệp thứ ba, chứa tất cả các lớp khởi tạo mà bạn cần:

samples.cpp:

// Ensure the full Array template definition can be seen
#include "Array.h"
#include "Array.cpp" // we're breaking best practices here, but only in this one place
 
// #include other .h and .cpp template definitions you need here
 
template class Array<int>; // Explicitly instantiate template Array<int>
template class Array<double>; // Explicitly instantiate template Array<double>
 
// instantiate other templates here

Lệnh “template class” làm cho trình biên dịch khởi tạo một cách rõ ràng lớp mẫu. Trong trường hợp trên, trình biên dịch sẽ in ra cả Array <int> và Array <double> bên trong samples.cpp. Bởi vì samples.cpp nằm trong dự án của chúng ta, nên điều này sẽ được biên dịch. Các hàm này sau đó có thể được liên kết từ nơi khác.

Phương pháp này hiệu quả hơn, nhưng yêu cầu duy trì file samples.cpp cho mỗi chương trình.