Hàm templates
Một mẫu hàm bao gồm một tiêu đề với các tham số mẫu (cú pháp đã được mô tả trước đó) và một định nghĩa hàm trong đó các tham số mẫu biểu thị các kiểu tùy ý.
Ví dụ đầu tiên, hãy xem xét hàm Swap
để hoán đổi hai phần tử mảng (TemplatesSorting.mq5
). Tham số mẫu T được sử dụng làm kiểu của biến mảng đầu vào, cũng như kiểu của biến cục bộ temp
.
template<typename T>
void Swap(T &array[], const int i, const int j)
{
const T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
2
3
4
5
6
7
Tất cả các câu lệnh và biểu thức trong phần thân hàm phải áp dụng được cho các kiểu thực tế mà mẫu sẽ được khởi tạo sau đó. Trong trường hợp này, toán tử gán '=' được sử dụng. Mặc dù nó luôn tồn tại cho các kiểu tích hợp, nhưng có thể cần phải nạp chồng rõ ràng cho các kiểu do người dùng định nghĩa.
Trình biên dịch tạo ra triển khai của toán tử sao chép cho các lớp và cấu trúc theo mặc định, nhưng nó có thể bị xóa ngầm hoặc rõ ràng (xem từ khóa delete
). Đặc biệt, như chúng ta đã thấy trong phần Ép kiểu đối tượng, việc có một trường hằng trong một lớp khiến trình biên dịch xóa tùy chọn sao chép ngầm của nó. Khi đó, hàm mẫu Swap
ở trên không thể được sử dụng cho các đối tượng của lớp này: trình biên dịch sẽ tạo ra lỗi.
Đối với các lớp/cấu trúc mà hàm Swap
hoạt động, mong muốn không chỉ có toán tử gán mà còn có hàm tạo sao chép, bởi vì khai báo biến temp
thực chất là một sự khởi tạo, không phải gán. Với hàm tạo sao chép, dòng đầu tiên của hàm được thực thi trong một lần (tạo temp
dựa trên array[i]
), trong khi nếu không có nó, hàm tạo mặc định sẽ được gọi trước, sau đó toán tử '=' sẽ được thực thi cho temp
.
Hãy xem cách hàm mẫu Swap
có thể được sử dụng trong thuật toán sắp xếp nhanh: một hàm mẫu khác QuickSort
triển khai nó.
template<typename T>
void QuickSort(T &array[], const int start = 0, int end = INT_MAX)
{
if(end == INT_MAX)
{
end = start + ArraySize(array) - 1;
}
if(start < end)
{
int pivot = start;
for(int i = start; i <= end; i++)
{
if(!(array[i] > array[end]))
{
Swap(array, i, pivot++);
}
}
--pivot;
QuickSort(array, start, pivot - 1);
QuickSort(array, pivot + 1, end);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Lưu ý rằng tham số T của mẫu QuickSort
chỉ định kiểu của tham số đầu vào array
, và mảng này sau đó được truyền vào mẫu Swap
. Do đó, suy ra kiểu T cho mẫu QuickSort
sẽ tự động xác định kiểu T cho mẫu Swap
.
Hàm tích hợp ArraySize
(giống như nhiều hàm khác) có thể hoạt động với các mảng thuộc bất kỳ kiểu nào: theo một nghĩa nào đó, nó cũng là một mẫu, mặc dù được triển khai trực tiếp trong terminal.
Việc sắp xếp được thực hiện nhờ toán tử so sánh '>' trong câu lệnh if
. Như đã đề cập trước đó, toán tử này phải được định nghĩa cho bất kỳ kiểu T nào đang được sắp xếp, vì nó áp dụng cho các phần tử của mảng kiểu T.
Hãy kiểm tra cách sắp xếp hoạt động cho các mảng của các kiểu tích hợp.
void OnStart()
{
double numbers[] = {34, 11, -7, 49, 15, -100, 11};
QuickSort(numbers);
ArrayPrint(numbers);
// -100.00000 -7.00000 11.00000 11.00000 15.00000 34.00000 49.00000
string messages[] = {"usd", "eur", "jpy", "gbp", "chf", "cad", "aud", "nzd"};
QuickSort(messages);
ArrayPrint(messages);
// "aud" "cad" "chf" "eur" "gbp" "jpy" "nzd" "usd"
}
2
3
4
5
6
7
8
9
10
11
12
Hai lời gọi đến hàm mẫu QuickSort
tự động suy ra kiểu của T dựa trên kiểu của các mảng được truyền vào. Kết quả là, chúng ta sẽ nhận được hai phiên bản của QuickSort
cho các kiểu double
và string
.
Để kiểm tra việc sắp xếp của một kiểu tùy chỉnh, hãy tạo một cấu trúc ABC với trường số nguyên x
, và điền nó bằng các số ngẫu nhiên trong hàm tạo. Việc nạp chồng toán tử '>' trong cấu trúc cũng rất quan trọng.
struct ABC
{
int x;
ABC()
{
x = rand();
}
bool operator>(const ABC &other) const
{
return x > other.x;
}
};
void OnStart()
{
...
ABC abc[10];
QuickSort(abc);
ArrayPrint(abc);
/* Kết quả mẫu:
[x]
[0] 1210
[1] 2458
[2] 10816
[3] 13148
[4] 15393
[5] 20788
[6] 24225
[7] 29919
[8] 32309
[9] 32589
*/
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Vì các giá trị cấu trúc được tạo ngẫu nhiên, chúng ta sẽ nhận được các kết quả khác nhau, nhưng chúng luôn được sắp xếp theo thứ tự tăng dần.
Trong trường hợp này, kiểu T cũng được suy ra tự động. Tuy nhiên, trong một số trường hợp, chỉ định rõ ràng là cách duy nhất để truyền kiểu vào hàm mẫu. Vì vậy, nếu một hàm mẫu phải trả về giá trị của một kiểu duy nhất (khác với kiểu của các tham số của nó) hoặc nếu không có tham số, thì nó chỉ có thể được chỉ định rõ ràng.
Ví dụ, hàm mẫu sau createInstance
yêu cầu kiểu được chỉ định rõ ràng trong lệnh gọi, vì không thể tự động "tính toán" kiểu T từ giá trị trả về. Nếu điều này không được thực hiện, trình biên dịch tạo ra lỗi "không khớp mẫu".
class Base
{
...
};
template<typename T>
T *createInstance()
{
T *object = new T(); // gọi hàm tạo
... // thiết lập đối tượng
return object;
}
void OnStart()
{
Base *p1 = createInstance(); // lỗi: không khớp mẫu
Base *p2 = createInstance<Base>(); // ok, chỉ thị rõ ràng
...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Nếu có nhiều tham số mẫu và kiểu của giá trị trả về không liên kết với bất kỳ tham số đầu vào nào của hàm, thì bạn cũng cần chỉ định một kiểu cụ thể khi gọi:
template<typename T,typename U>
T MyCast(const U u)
{
return (T)u;
}
void OnStart()
{
double d = MyCast<double,string>("123.0");
string f = MyCast<string,double>(123.0);
}
2
3
4
5
6
7
8
9
10
11
Lưu ý rằng nếu các kiểu cho mẫu được chỉ định rõ ràng, thì điều này được yêu cầu cho tất cả các tham số, mặc dù tham số thứ hai U có thể được suy ra từ đối số được truyền vào.
Sau khi trình biên dịch đã tạo tất cả các phiên bản của hàm mẫu, chúng tham gia vào quy trình tiêu chuẩn để chọn ứng cử viên tốt nhất từ tất cả các nạp chồng hàm cùng tên và số lượng tham số phù hợp. Trong tất cả các tùy chọn nạp chồng (bao gồm cả các phiên bản mẫu đã tạo), cái gần nhất về kiểu (với số lượng chuyển đổi ít nhất) được chọn.
Nếu một hàm mẫu có một số tham số đầu vào của các kiểu cụ thể, thì nó chỉ được coi là ứng cử viên nếu các kiểu này hoàn toàn khớp với các đối số: bất kỳ nhu cầu chuyển đổi nào cũng sẽ khiến mẫu bị "loại bỏ" vì không phù hợp.
Các nạp chồng không phải mẫu được ưu tiên hơn các nạp chồng mẫu, các nạp chồng chuyên biệt hơn ("tập trung hẹp") "thắng" các nạp chồng mẫu.
Nếu đối số mẫu (kiểu) được chỉ định rõ ràng, thì các quy tắc cho ép kiểu ngầm được áp dụng cho đối số hàm tương ứng (giá trị được truyền), nếu cần, nếu các kiểu này khác nhau.
Nếu nhiều biến thể của một hàm khớp ngang nhau, chúng ta sẽ nhận được lỗi "lời gọi không rõ ràng đến một hàm nạp chồng với cùng tham số".
Ví dụ, nếu ngoài mẫu MyCast
, một hàm được định nghĩa để chuyển đổi chuỗi thành kiểu boolean:
bool MyCast(const string u)
{
return u == "true";
}
2
3
4
thì việc gọi MyCast<double,string>("123.0")
sẽ bắt đầu ném lỗi đã chỉ ra, vì hai hàm chỉ khác nhau ở giá trị trả về:
'MyCast<double,string>' - lời gọi không rõ ràng đến hàm nạp chồng với cùng tham số
có thể là một trong 2 hàm
double MyCast<double,string>(const string)
bool MyCast(const string)
2
3
4
Khi mô tả các hàm mẫu, nên bao gồm tất cả các tham số mẫu trong các tham số hàm. Kiểu chỉ có thể được suy ra từ các đối số, không phải từ giá trị trả về.
Nếu một hàm có tham số kiểu mẫu T với giá trị mặc định, và đối số tương ứng bị bỏ qua khi gọi, thì trình biên dịch cũng sẽ không thể suy ra kiểu của T và ném lỗi "không thể áp dụng mẫu".
class Base
{
public:
Base(const Base *source = NULL) { }
static Base *type;
};
static Base* Base::type;
template<typename T>
T *createInstanceFrom(T *origin = NULL)
{
T *object = new T(origin);
return object;
}
void OnStart()
{
Base *p1 = createInstanceFrom(); // lỗi: không thể áp dụng mẫu
Base *p2 = createInstanceFrom(Base::type); // ok, tự động phát hiện từ đối số
Base *p3 = createInstanceFrom<Base>(); // ok, chỉ thị rõ ràng, đối số bị bỏ qua
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22