Đặc điểm của các kiểu tích hợp và kiểu đối tượng trong templates
Cần lưu ý rằng 3 khía cạnh quan trọng áp đặt các hạn chế đối với khả năng áp dụng của các kiểu trong mẫu:
- Liệu kiểu đó là kiểu tích hợp hay do người dùng định nghĩa (các kiểu do người dùng định nghĩa yêu cầu truyền tham số qua tham chiếu, còn các kiểu tích hợp không cho phép truyền hằng số qua tham chiếu);
- Liệu kiểu đối tượng có phải là lớp hay không (chỉ các lớp hỗ trợ con trỏ);
- Tập hợp các phép toán được thực hiện trên dữ liệu của các kiểu tương ứng trong thuật toán mẫu.
Giả sử chúng ta có một cấu trúc Dummy (xem script TemplatesMax.mq5):
struct Dummy
{
int x;
};2
3
4
Nếu chúng ta cố gắng gọi hàm Max cho hai thể hiện của cấu trúc này, chúng ta sẽ nhận được một loạt thông báo lỗi, với các lỗi chính như sau: "đối tượng chỉ có thể được truyền qua tham chiếu" và "không thể áp dụng mẫu."
// LỖI:
// 'object1' - đối tượng chỉ được truyền qua tham chiếu
// 'Max' - không thể áp dụng mẫu
Dummy object1, object2;
Max(object1, object2);2
3
4
5
Đỉnh điểm của vấn đề là việc truyền tham số của hàm mẫu theo giá trị, và phương thức này không tương thích với bất kỳ kiểu đối tượng nào. Để giải quyết, bạn có thể thay đổi kiểu của tham số thành tham chiếu:
template<typename T>
T Max(T &value1, T &value2)
{
return value1 > value2 ? value1 : value2;
}2
3
4
5
Lỗi cũ sẽ biến mất, nhưng sau đó chúng ta sẽ gặp một lỗi mới: "'>' - sử dụng phép toán không hợp lệ." Vấn đề là mẫu Max có một biểu thức với toán tử so sánh '>'. Do đó, nếu một kiểu tùy chỉnh được thay thế vào mẫu, toán tử '>' phải được nạp chồng trong mẫu (và cấu trúc Dummy không có nó: chúng ta sẽ sớm giải quyết điều này). Đối với các hàm phức tạp hơn, bạn có thể sẽ cần nạp chồng một số lượng lớn toán tử hơn. May mắn thay, trình biên dịch cho bạn biết chính xác những gì còn thiếu.
Tuy nhiên, việc thay đổi cách truyền tham số hàm qua tham chiếu còn dẫn đến việc lời gọi trước đó không hoạt động như vậy:
Print(Max<ulong(1000, 10000000));Bây giờ nó tạo ra lỗi: "tham số được truyền dưới dạng tham chiếu, cần biến." Do đó, hàm mẫu của chúng ta đã ngừng hoạt động với các hằng số và các giá trị tạm thời khác (đặc biệt, không thể truyền trực tiếp một biểu thức hoặc kết quả của việc gọi hàm khác vào nó).
Có thể nghĩ rằng cách giải quyết phổ quát cho tình huống này là nạp chồng hàm mẫu, tức là định nghĩa cả hai tùy chọn, chỉ khác nhau ở dấu & trong tham số:
template<typename T>
T Max(T &value1, T &value2)
{
return value1 > value2 ? value1 : value2;
}
template<typename T>
T Max(T value1, T value2)
{
return value1 > value2 ? value1 : value2;
}2
3
4
5
6
7
8
9
10
11
Nhưng điều này không hoạt động. Bây giờ trình biên dịch báo lỗi "nạp chồng hàm không rõ ràng với cùng tham số":
'Max' - 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
T Max(T&,T&)
T Max(T,T)2
3
4
Nạp chồng cuối cùng, hoạt động được, sẽ yêu cầu thêm bộ sửa đổi const vào các tham chiếu. Đồng thời, chúng ta đã thêm toán tử Print vào mẫu Max để có thể thấy trong nhật ký nạp chồng nào đang được gọi và tham số kiểu T tương ứng với gì.
template<typename T>
T Max(const T &value1, const T &value2)
{
Print(__FUNCSIG__, " T=", typename(T));
return value1 > value2 ? value1 : value2;
}
template<typename T>
T Max(T value1, T value2)
{
Print(__FUNCSIG__, " T=", typename(T));
return value1 > value2 ? value1 : value2;
}
struct Dummy
{
int x;
bool operator>(const Dummy &other) const
{
return x > other.x;
}
};2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Chúng ta cũng đã triển khai nạp chồng toán tử '>' trong cấu trúc Dummy. Do đó, tất cả các lời gọi hàm Max trong script thử nghiệm đều hoàn thành thành công: cả cho các kiểu tích hợp và do người dùng định nghĩa, cũng như cho hằng số và biến. Các đầu ra được ghi vào nhật ký:
double Max<double>(double,double) T=double
1.0
datetime Max<datetime>(datetime,datetime) T=datetime
2021.10.10 00:00:00
ulong OnStart::Max<ulong>(ulong,ulong) T=ulong
10000000
Dummy Max<Dummy>(const Dummy&,const Dummy&) T=Dummy2
3
4
5
6
7
Một độc giả chú ý sẽ nhận thấy rằng giờ đây chúng ta có hai hàm giống nhau chỉ khác nhau ở cách truyền tham số (theo giá trị và theo tham chiếu), và đây chính là tình huống mà việc sử dụng mẫu nhằm chống lại. Sự trùng lặp như vậy có thể tốn kém nếu phần thân hàm không đơn giản như của chúng ta. Điều này có thể được giải quyết bằng các phương pháp thông thường: tách triển khai thành một hàm riêng và gọi nó từ cả hai "nạp chồng", hoặc gọi một "nạp chồng" từ cái kia (một tham số tùy chọn là cần thiết để tránh phiên bản đầu tiên của Max tự gọi chính nó và dẫn đến tràn ngăn xếp):
template<typename T>
T Max(T value1, T value2)
{
// gọi hàm với tham số qua tham chiếu
return Max(value1, value2, true);
}
template<typename T>
T Max(const T &value1, const T &value2, const bool ref = false)
{
return (T)(value1 > value2 ? value1 : value2);
}2
3
4
5
6
7
8
9
10
11
12
Chúng ta vẫn phải xem xét một điểm nữa liên quan đến các kiểu do người dùng định nghĩa, đó là việc sử dụng con trỏ trong mẫu (nhớ rằng chúng chỉ áp dụng cho các đối tượng lớp). Hãy tạo một lớp đơn giản Data và thử gọi hàm mẫu Max cho các con trỏ đến các đối tượng của nó.
class Data
{
public:
int x;
bool operator>(const Data &other) const
{
return x > other.x;
}
};
void OnStart()
{
...
Data *pointer1 = new Data();
Data *pointer2 = new Data();
Max(pointer1, pointer2);
delete pointer1;
delete pointer2;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Chúng ta sẽ thấy trong nhật ký rằng 'T=Data*', tức là thuộc tính con trỏ đã nằm trong kiểu nội tuyến. Điều này cho thấy rằng, nếu cần, bạn có thể viết một nạp chồng khác của hàm mẫu, chỉ chịu trách nhiệm cho các con trỏ.
template<typename T>
T *Max(T *value1, T *value2)
{
Print(__FUNCSIG__, " T=", typename(T));
return value1 > value2 ? value1 : value2;
}2
3
4
5
6
Trong trường hợp này, thuộc tính con trỏ '*' đã có trong các tham số mẫu, và do đó suy ra kiểu dẫn đến 'T=Data'. Cách tiếp cận này cho phép cung cấp một triển khai mẫu riêng cho các con trỏ.
Nếu có nhiều mẫu phù hợp để tạo một phiên bản với các kiểu cụ thể, phiên bản chuyên biệt nhất của mẫu sẽ được chọn. Đặc biệt, khi gọi hàm Max với các đối số là con trỏ, hai mẫu với tham số T (T=Data*) và T* (T=Data), nhưng vì cái đầu tiên có thể nhận cả giá trị và con trỏ, nó tổng quát hơn cái sau, cái chỉ hoạt động với con trỏ. Do đó, cái thứ hai sẽ được chọn cho con trỏ. Nói cách khác, càng ít bộ sửa đổi trong kiểu thực tế được thay thế cho T, thì biến thể mẫu càng được ưu tiên. Ngoài thuộc tính con trỏ '*', điều này cũng bao gồm bộ sửa đổi const. Các tham số const T* hoặc const T chuyên biệt hơn so với chỉ T* hoặc T, tương ứng.
