Thật đáng để xem qua cách các hàm mẫu(template) được triển khai trong C ++, bởi vì các bài học trong tương lai sẽ xây dựng một số khái niệm này. Hóa ra C ++ không biên dịch hàm mẫu trực tiếp. Thay vào đó, tại thời gian biên dịch, khi trình biên dịch bắt gặp một cuộc gọi đến một hàm mẫu, nó sẽ sao chép hàm mẫu và thay thế các tham số kiểu mẫu bằng các kiểu thực tế. Hàm với các kiểu thực tế được gọi là một thể hiện mẫu hàm .

Chúng ta hãy xem một ví dụ về quá trình này. Đầu tiên, chúng ta có một hàm templated:

/**
* 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/
*/

template <typename T> // this is the template parameter declaration
const T& max(const T& x, const T& y)
{
    return (x > y) ? x : y;
}

Khi biên dịch chương trình của bạn, trình biên dịch sẽ gặp một lệnh gọi đến hàm templated:

int i{ max(3, 7) }; // calls max(int, int)

Trình biên dịch nói, ồ, chúng ta muốn gọi max (int, int). Trình biên dịch sao chép hàm mẫu và tạo ra cá thể mẫu max (int, int):

const int& max(const int &x, const int &y)
{
    return (x > y) ? x : y;
}

Giờ đây, đây là một hàm bình thường, có thể được biên dịch thành ngôn ngữ máy.

Bây giờ, hãy nói trong code của bạn, bạn đã gọi lại max() bằng một kiểu khác:

double d{ max(6.34, 18.523) }; // calls max(double, double)

C ++ tự động tạo một thể hiện mẫu cho max (double, double):

const double& max(const double &x, const double &y)
{
    return (x > y) ? x : y;
}

và sau đó biên dịch nó.

Trình biên dịch đủ thông minh để biết nó chỉ cần tạo một thể hiện mẫu cho mỗi bộ tham số kiểu duy nhất (trên mỗi file). Cũng đáng lưu ý rằng nếu bạn tạo một hàm mẫu nhưng không gọi nó, sẽ không có phiên bản mẫu nào được tạo.

1. Toán tử, gọi hàm và các mẫu hàm

Các hàm mẫu sẽ hoạt động với cả các kiểu tích hợp (ví dụ: char, int, double, v.v.) và các lớp, với một cảnh báo. Khi trình biên dịch biên dịch thể hiện mẫu, nó biên dịch nó giống như một hàm bình thường. Trong một hàm bình thường, mọi toán tử hoặc hàm gọi mà bạn sử dụng với các kiểu của bạn phải được xác định hoặc bạn sẽ gặp lỗi trình biên dịch. Tương tự, bất kỳ toán tử hoặc lệnh gọi hàm nào trong hàm mẫu của bạn phải được xác định cho bất kỳ kiểu nào khi hàm mẫu được khởi tạo. Chúng ta hãy xem xét điều này chi tiết hơn.

Đầu tiên, chúng ta sẽ tạo một lớp đơn giản:

class Cents
{
private:
    int m_cents;
public:
    Cents(int cents)
        : m_cents{ cents }
    {
    }
};

Bây giờ, hãy xem điều gì xảy ra khi chúng ta thử gọi hàm max () templated của mình bằng lớp Cents:

/**
* 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/
*/

template <typename T> // this is the template parameter declaration
const T& max(const T& x, const T& y)
{
    return (x > y) ? x : y;
}
 
class Cents
{
private:
    int m_cents;
public:
    Cents(int cents)
        : m_cents{ cents }
    {
    }
};
 
int main()
{
    Cents nickle{ 5 };
    Cents dime{ 10 };
 
    Cents bigger{ max(nickle, dime) };
 
    return 0;
}

C ++ sẽ tạo một cá thể mẫu cho max () trông như thế này:

const Cents& max(const Cents &x, const Cents &y)
{
    return (x > y) ? x : y;
}

Và sau đó nó sẽ cố gắng biên dịch hàm này. Xem vấn đề ở đây? C ++ không thể đánh giá x > y, vì xy là các đối tượng lớp Cents và không biết cách so sánh chúng. Do đó, điều này sẽ tạo ra một lỗi biên dịch trông khá thuần phục, như thế này:

1>c:\consoleapplication1\main.cpp(4): error C2676: binary '>': 'const Cents' does not define this operator or a conversion to a type acceptable to the predefined operator
1>  c:\consoleapplication1\main.cpp(23): note: see reference to function template instantiation 'const T &max(const T &,const T &)' being compiled
1>          with
1>          [
1>              T=Cents
1>          ]

Thông báo lỗi đầu hàng chỉ ra thực tế là không có toán tử quá tải > cho lớp Cents. Lỗi dưới cùng chỉ ra lệnh gọi hàm templated đã sinh ra lỗi, cùng với kiểu tham số templated.

Để giải quyết vấn đề này, chỉ cần quá tải toán tử > cho bất kỳ lớp nào chúng ta muốn sử dụng max() với:

class Cents
{
private:
    int m_cents;
public:
    Cents(int cents)
        : m_cents{ cents }
    {
    }
 
    friend bool operator>(const Cents &c1, const Cents &c2)
    {
        return (c1.m_cents > c2.m_cents);
    }
};

Bây giờ C ++ sẽ biết cách so sánh x > ykhi x và y là đối tượng của lớp Cents! Do đó, hàm max() của chúng ta sẽ hoạt động với hai đối tượng kiểu Cents.

2. Một vi dụ khac

Hãy làm thêm một ví dụ về hàm mẫu. Hàm Mẫu sau sẽ tính trung bình của một số đối tượng trong một mảng:

template <class T>
T average(T *array, int length)
{
    T sum(0);
 
    for (int count{ 0 }; count < length; ++count)
        sum += array[count];
 
    sum /= length;
    return sum;
}

Bây giờ hãy xem nó hoạt động:

#include <iostream>
 
template <class T>
T average(T *array, int length)
{
    T sum(0);
    for (int count{ 0 }; count < length; ++count)
        sum += array[count];
 
    sum /= length;
    return sum;
}
 
int main()
{
    int array1[]{ 5, 3, 2, 1, 4 };
    std::cout << average(array1, 5) << '\n';
 
    double array2[]{ 3.12, 3.45, 9.23, 6.34 };
    std::cout << average(array2, 4) << '\n';
 
    return 0;
}

Điều này tạo ra các giá trị:

3
5.535

Như bạn có thể thấy, nó hoạt động rất tốt cho các loại tích hợp!

Điều đáng chú ý là vì kiểu trả về của chúng ta là cùng một kiểu khuôn mẫu với các phần tử mảng của chúng ta, làm trung bình số nguyên sẽ tạo ra kết quả số nguyên. Điều này tương tự như cách thực hiện phép chia số nguyên sẽ tạo ra kết quả số nguyên. Không có gì sai khi chúng ta đã định nghĩa mọi thứ để hoạt động theo cách đó, nhưng nó có thể là bất ngờ, vì vậy một comment sẽ tốt cho người dùng của lớp sẽ không được đưa ra ở đây.

Bây giờ hãy xem điều gì xảy ra khi chúng ta gọi hàm này trên lớp Cents:

/**
* 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 <iostream>
 
class Cents
{
private:
    int m_cents;
public:
    Cents(int cents)
        : m_cents{ cents }
    {
    }
 
    friend bool operator>(const Cents &c1, const Cents &c2)
    {
        return (c1.m_cents > c2.m_cents);
    }
};
 
template <class T>
T average(T *array, int length)
{
    T sum(0);
    for (int count{ 0 }; count < length; ++count)
        sum += array[count];
 
    sum /= length;
    return sum;
}
 
int main()
{
    Cents array3[]{ Cents(5), Cents(10), Cents(15), Cents(14) };
    std::cout << average(array3, 4) << '\n';
 
    return 0;
}

Trình biên dịch trở nên điên loạn và tạo ra rất nhiều thông báo lỗi!

example.cpp

(33): error C2679: binary '<<': no operator found which takes a right-hand operand of type 'T' (or there is no acceptable conversion)

        with

        [

            T=Cents

        ]

C:/data/msvc/14.22.27905/include\ostream(437): note: could be 'std::basic_ostream> &std::basic_ostream>::operator <<(std::basic_streambuf> *)'
C:/data/msvc/14.22.27905/include\ostream(412): note: or       'std::basic_ostream> &std::basic_ostream>::operator <<(const void *)'
C:/data/msvc/14.22.27905/include\ostream(394): note: or       'std::basic_ostream> &std::basic_ostream>::operator <<(long double)'
C:/data/msvc/14.22.27905/include\ostream(376): note: or       'std::basic_ostream> &std::basic_ostream>::operator <<(double)'
C:/data/msvc/14.22.27905/include\ostream(358): note: or       'std::basic_ostream> &std::basic_ostream>::operator <<(float)'
C:/data/msvc/14.22.27905/include\ostream(340): note: or       'std::basic_ostream> &std::basic_ostream>::operator <<(unsigned __int64)'
C:/data/msvc/14.22.27905/include\ostream(322): note: or       'std::basic_ostream> &std::basic_ostream>::operator <<(__int64)'
C:/data/msvc/14.22.27905/include\ostream(304): note: or       'std::basic_ostream> &std::basic_ostream>::operator <<(unsigned long)'
C:/data/msvc/14.22.27905/include\ostream(286): note: or       'std::basic_ostream> &std::basic_ostream>::operator <<(long)'
C:/data/msvc/14.22.27905/include\ostream(268): note: or       'std::basic_ostream> &std::basic_ostream>::operator <<(unsigned int)'
C:/data/msvc/14.22.27905/include\ostream(248): note: or       'std::basic_ostream> &std::basic_ostream>::operator <<(int)'
C:/data/msvc/14.22.27905/include\ostream(230): note: or       'std::basic_ostream> &std::basic_ostream>::operator <<(unsigned short)'
C:/data/msvc/14.22.27905/include\ostream(202): note: or       'std::basic_ostream> &std::basic_ostream>::operator <<(short)'
C:/data/msvc/14.22.27905/include\ostream(184): note: or       'std::basic_ostream> &std::basic_ostream>::operator <<(bool)'
C:/data/msvc/14.22.27905/include\ostream(179): note: or       'std::basic_ostream> &std::basic_ostream>::operator <<(std::ios_base &(__cdecl *)(std::ios_base &))'
C:/data/msvc/14.22.27905/include\ostream(174): note: or       'std::basic_ostream> &std::basic_ostream>::operator <<(std::basic_ios> &(__cdecl *)(std::basic_ios> &))'
C:/data/msvc/14.22.27905/include\ostream(169): note: or       'std::basic_ostream> &std::basic_ostream>::operator <<(std::basic_ostream> &(__cdecl *)(std::basic_ostream> &))'
C:/data/msvc/14.22.27905/include\ostream(613): note: or       'std::basic_ostream> &std::operator <<>(std::basic_ostream> &,const char *)'
C:/data/msvc/14.22.27905/include\ostream(658): note: or       'std::basic_ostream> &std::operator <<>(std::basic_ostream> &,char)'
C:/data/msvc/14.22.27905/include\ostream(694): note: or       'std::basic_ostream> &std::operator <<>(std::basic_ostream> &,const char *)'
C:/data/msvc/14.22.27905/include\ostream(739): note: or       'std::basic_ostream> &std::operator <<>(std::basic_ostream> &,char)'
C:/data/msvc/14.22.27905/include\ostream(858): note: or       'std::basic_ostream> &std::operator <<>(std::basic_ostream> &,const signed char *)'
C:/data/msvc/14.22.27905/include\ostream(864): note: or       'std::basic_ostream> &std::operator <<>(std::basic_ostream> &,signed char)'
C:/data/msvc/14.22.27905/include\ostream(870): note: or       'std::basic_ostream> &std::operator <<>(std::basic_ostream> &,const unsigned char *)'
C:/data/msvc/14.22.27905/include\ostream(876): note: or       'std::basic_ostream> &std::operator <<>(std::basic_ostream> &,unsigned char)'
C:/data/msvc/14.22.27905/include\ostream(931): note: or       'std::basic_ostream> &std::operator <<>(std::basic_ostream> &,const std::error_code &)'
(33): note: while trying to match the argument list '(std::ostream, T)'

        with

        [

            T=Cents

        ]

Compiler returned: 2

Nhớ những gì tôi nói về thông báo lỗi? Chúng ta nhấn nhẹ! Mặc dù trông đáng sợ, nhưng những điều này thực sự khá đơn giản. Dòng đầu tiên cho bạn biết rằng nó không thể tìm thấy toán tử quá tải << cho lớp Cents. Tất cả các dòng ở giữa là tất cả các chức năng khác nhau mà nó đã cố gắng khớp với nhưng không thành công. Lỗi cuối cùng chỉ ra lệnh gọi hàm đã sinh ra danh sách lỗi này.

Hãy nhớ rằng Average() trả về một đối tượng Cents và chúng ta đang cố gắng truyền đối tượng đó đến std :: cout bằng toán tử <<. Tuy nhiên, chúng ta chưa định nghĩa toán tử << cho lớp Cents của chúng ta. Hãy làm điều đó:

class Cents
{
private:
    int m_cents;
public:
    Cents(int cents)
        : m_cents{ cents }
    {
    }
 
    friend bool operator>(const Cents &c1, const Cents &c2)
    {
        return (c1.m_cents > c2.m_cents);
    }
 
    friend std::ostream& operator<< (std::ostream &out, const Cents &cents)
    {
        out << cents.m_cents << " cents ";
        return out;
    }
};

Nếu chúng ta biên dịch lại, chúng ta sẽ gặp một lỗi khác:

c:test.cpp(14) : error C2676: binary '+=' : 'Cents' does not define this operator or a conversion to a type acceptable to the predefined operator

Lỗi này thực sự được gây ra bởi cá thể hàm mẫu được tạo khi chúng ta gọi average(Cents*, int). Hãy nhớ rằng khi chúng ta gọi một hàm templated, các trình biên dịch tạo ra một bản sao của hàm trong đó các tham số kiểu mẫu (các kiểu giữ chỗ) đã được thay thế bằng các kiểu thực tế trong lệnh gọi hàm. Đây là ví dụ hàm mẫu cho Average() khi T là đối tượng Cents:

template <class T>
Cents average(Cents *array, int length)
{
    Cents sum(0);
    for (int count{ 0 }; count < length; ++count)
        sum += array[count];
 
    sum /= length;
    return sum;
}

Lý do chúng ta nhận được thông báo lỗi là vì dòng sau:

sum += array[count];

Trong trường hợp này, sum là một đối tượng Cents, nhưng chúng ta chưa định nghĩa toán tử + = cho các đối tượng Cents! Chúng ta sẽ cần xác định hàm này để có giá trị trung bình () để có thể làm việc với Cents. Nhìn về phía trước, chúng ta có thể thấy rằng average() cũng sử dụng toán tử / =, vì vậy chúng ta sẽ tiếp tục và định nghĩa điều đó:

/**
* 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/
*/

class Cents
{
private:
    int m_cents;
public:
    Cents(int cents)
        : m_cents{ cents }
    {
    }
 
    friend bool operator>(const Cents &c1, const Cents &c2)
    {
        return (c1.m_cents > c2.m_cents);
    }
 
    friend std::ostream& operator<< (std::ostream &out, const Cents &cents)
    {
        out << cents.m_cents << " cents ";
        return out;
    }
 
    Cents& operator+=(const Cents &cents)
    {
        m_cents += cents.m_cents;
        return *this;
    }
 
    Cents& operator/=(int value)
    {
        m_cents /= value;
        return *this;
    }
};

Cuối cùng, code của chúng ta sẽ biên dịch và chạy! Đây là kết quả:

11 cents

Nếu điều này có vẻ như rất nhiều công việc, thì điều đó thực sự chỉ bởi vì lớp Cents của chúng ta quá trơ xương để bắt đầu. Điểm mấu chốt ở đây thực sự là chúng ta hoàn toàn không phải sửa đổi average() để làm cho nó hoạt động với các đối tượng thuộc loại Cents (hoặc bất kỳ kiểu nào khác). Chúng ta chỉ đơn giản là phải định nghĩa các toán tử được sử dụng để triển khai hàm average() cho lớp Cents và trình biên dịch sẽ giải quyết phần còn lại!