Phương thức templates
Không chỉ toàn bộ kiểu đối tượng có thể là một mẫu, mà một phương thức riêng lẻ của nó — đơn giản hoặc tĩnh — cũng có thể là một mẫu. Ngoại lệ là các phương thức ảo: chúng không thể được tạo thành mẫu. Điều này dẫn đến việc các phương thức mẫu không thể được khai báo bên trong giao diện. Tuy nhiên, bản thân các giao diện có thể được tạo thành mẫu, và các phương thức ảo có thể xuất hiện trong các mẫu lớp.
Khi một phương thức mẫu nằm trong một mẫu lớp/cấu trúc, các tham số của cả hai mẫu phải khác nhau. Nếu có nhiều phương thức mẫu, các tham số của chúng không liên quan gì đến nhau và có thể có cùng tên.
Một phương thức mẫu được khai báo tương tự như một mẫu hàm, nhưng chỉ trong bối cảnh của một lớp, cấu trúc hoặc liên minh (có thể là mẫu hoặc không).
[ template<typename T ⌠, typename Ti ...] > ]
class class_name
{
...
template<typename U [, typename Ui ...] >
type method_name(parameters_with_types_T_and_U)
{
}
};
2
3
4
5
6
7
8
9
Các tham số, giá trị trả về và thân phương thức có thể sử dụng các kiểu T (chung cho lớp) và U (cụ thể cho phương thức).
Một phiên bản của phương thức cho một tổ hợp tham số cụ thể chỉ được tạo ra khi nó được gọi trong mã chương trình.
Trong phần trước, chúng ta đã mô tả lớp mẫu AutoPtr
để lưu trữ và giải phóng một con trỏ đơn. Khi có nhiều con trỏ cùng loại, việc đặt chúng vào một đối tượng chứa là tiện lợi. Hãy tạo một mẫu đơn giản với chức năng tương tự — lớp SimpleArray
(SimpleArray.mqh
). Để không trùng lặp chức năng kiểm soát giải phóng bộ nhớ động, chúng ta sẽ đưa vào hợp đồng lớp rằng nó được thiết kế để lưu trữ các giá trị và đối tượng, nhưng không phải con trỏ. Để lưu trữ các con trỏ, chúng ta sẽ đặt chúng vào các đối tượng AutoPtr
, và sau đó đặt vào chứa.
Điều này còn có một tác động tích cực khác: vì đối tượng AutoPtr
nhỏ, việc sao chép nó rất dễ dàng (mà không tốn quá nhiều tài nguyên), điều thường xảy ra khi dữ liệu được trao đổi giữa các hàm. Các đối tượng của những lớp ứng dụng mà AutoPtr
trỏ tới có thể lớn, và thậm chí không cần phải triển khai hàm tạo sao chép riêng trong chúng.
Tất nhiên, việc trả về con trỏ từ các hàm sẽ rẻ hơn, nhưng sau đó bạn cần phải phát minh lại các phương tiện kiểm soát giải phóng bộ nhớ. Do đó, sử dụng giải pháp sẵn có dưới dạng AutoPtr
sẽ đơn giản hơn.
Đối với các đối tượng bên trong chứa, chúng ta sẽ tạo mảng data
của kiểu mẫu T.
template<typename T>
class SimpleArray
{
protected:
T data[];
...
2
3
4
5
6
Vì một trong những thao tác chính của một chứa là thêm một phần tử, hãy cung cấp một hàm trợ giúp để mở rộng mảng.
int expand()
{
const int n = ArraySize(data);
ArrayResize(data, n + 1);
return n;
}
2
3
4
5
6
Chúng ta sẽ thêm các phần tử trực tiếp thông qua toán tử nạp chồng <<
. Nó sử dụng tham số mẫu chung T.
public:
SimpleArray *operator<<(const T &r)
{
data[expand()] = (T)r;
return &this;
}
2
3
4
5
6
Tùy chọn này nhận một giá trị qua tham chiếu, tức là một biến hoặc một đối tượng. Bạn nên chú ý đến điều này ngay bây giờ, và lý do tại sao điều này quan trọng sẽ trở nên rõ ràng trong vài phút nữa.
Việc đọc các phần tử được thực hiện bằng cách nạp chồng toán tử []
(nó có độ ưu tiên cao nhất và do đó không yêu cầu sử dụng dấu ngoặc trong các biểu thức).
T operator[](int i) const
{
return data[i];
}
2
3
4
Đầu tiên, hãy đảm bảo rằng lớp hoạt động trên ví dụ của cấu trúc.
struct Properties
{
int x;
string s;
};
2
3
4
5
Để làm điều này, chúng ta sẽ mô tả một chứa cho cấu trúc trong hàm OnStart
và đặt một đối tượng (TemplatesSimpleArray.mq5
) vào đó.
void OnStart()
{
SimpleArray<Properties> arrayStructs;
Properties prop = {12345, "abc"};
arrayStructs << prop;
Print(arrayStructs[0].x, " ", arrayStructs[0].s);
...
}
2
3
4
5
6
7
8
Ghi nhật ký gỡ lỗi cho phép bạn xác minh rằng cấu trúc nằm trong chứa.
Bây giờ hãy thử lưu trữ một số số trong chứa.
SimpleArray<double> arrayNumbers;
arrayNumbers << 1.0 << 2.0 << 3.0;
2
Thật không may, chúng ta sẽ gặp lỗi "parameter passed as reference, variable expected", xảy ra chính xác trong toán tử nạp chồng <<
.
Chúng ta cần một nạp chồng với việc truyền tham số theo giá trị. Tuy nhiên, chúng ta không thể chỉ viết một phương thức tương tự mà không có const
và &
:
SimpleArray *operator<<(T r)
{
data[expand()] = (T)r;
return &this;
}
2
3
4
5
Nếu bạn làm điều này, biến thể mới sẽ dẫn đến một mẫu không thể biên dịch cho các kiểu đối tượng: sau cùng, các đối tượng cần được truyền chỉ qua tham chiếu. Ngay cả khi hàm không được sử dụng cho các đối tượng, nó vẫn hiện diện trong lớp. Do đó, chúng ta sẽ định nghĩa phương thức mới dưới dạng một mẫu với tham số riêng của nó.
template<typename T>
class SimpleArray
{
...
template<typename U>
SimpleArray *operator<<(U u)
{
data[expand()] = (T)u;
return &this;
}
2
3
4
5
6
7
8
9
10
Phương thức này sẽ chỉ xuất hiện trong lớp nếu một thứ gì đó được truyền theo giá trị vào toán tử <<
, có nghĩa là chắc chắn không phải là một đối tượng. Đúng vậy, chúng ta không thể đảm bảo rằng T và U là giống nhau, vì vậy một ép kiểu rõ ràng (T)u
được thực hiện. Đối với các kiểu tích hợp (nếu hai kiểu không khớp), trong một số tổ hợp, việc chuyển đổi với mất độ chính xác là có thể, nhưng mã chắc chắn sẽ biên dịch. Ngoại lệ duy nhất là cấm chuyển đổi chuỗi thành kiểu boolean, nhưng không có khả năng chứa sẽ được sử dụng cho mảng bool
, vì vậy hạn chế này không đáng kể. Những ai muốn có thể giải quyết vấn đề này.
Với phương thức mẫu mới, chứa SimpleArray<double>
hoạt động như kỳ vọng và không xung đột với SimpleArray<Properties>
vì hai phiên bản mẫu có sự khác biệt trong mã nguồn được tạo ra.
Cuối cùng, hãy kiểm tra chứa với các đối tượng AutoPtr
. Để làm điều này, hãy chuẩn bị một lớp đơn giản Dummy
sẽ "cung cấp" các đối tượng cho các con trỏ bên trong AutoPtr
.
class Dummy
{
int x;
public:
Dummy(int i) : x(i) { }
int value() const
{
return x;
}
};
2
3
4
5
6
7
8
9
10
Bên trong hàm OnStart
, hãy tạo một chứa SimpleArray<AutoPtr<Dummy>>
và điền vào nó.
void OnStart()
{
SimpleArray<AutoPtr<Dummy>> arrayObjects;
AutoPtr<Dummy> ptr = new Dummy(20);
arrayObjects << ptr;
arrayObjects << AutoPtr<Dummy>(new Dummy(30));
Print(arrayObjects[0][].value());
Print(arrayObjects[1][].value());
}
2
3
4
5
6
7
8
9
Hãy nhớ rằng trong AutoPtr
toán tử []
được sử dụng để trả về một con trỏ được lưu trữ, vì vậy arrayObjects[0][]
có nghĩa là: trả về phần tử thứ 0 của mảng data
trong SimpleArray
, tức là đối tượng AutoPtr
, và sau đó cặp dấu ngoặc vuông thứ hai được áp dụng cho khối lượng, dẫn đến một con trỏ Dummy*. Tiếp theo, chúng ta có thể làm việc với tất cả các thuộc tính của đối tượng này: trong trường hợp này, chúng ta lấy giá trị hiện tại của trường x.
Vì Dummy
không có hàm tạo sao chép, bạn không thể sử dụng chứa để lưu trữ trực tiếp các đối tượng này mà không có AutoPtr
.
// LỖI:
// đối tượng của 'Dummy' không thể được trả về,
// hàm tạo sao chép 'Dummy::Dummy(const Dummy &)' không được tìm thấy
SimpleArray<Dummy> bad;
2
3
4
Nhưng một người dùng thông minh có thể đoán ra cách để vượt qua điều này.
SimpleArray<Dummy*> bad;
bad << new Dummy(0);
2
Mã này sẽ biên dịch và chạy. Tuy nhiên, "giải pháp" này chứa một vấn đề: SimpleArray
không biết cách kiểm soát các con trỏ, và do đó, khi chương trình thoát, một rò rỉ bộ nhớ được phát hiện.
1 undeleted objects left
1 object of type Dummy left
24 bytes of leaked memory
2
3
Chúng ta, với tư cách là nhà phát triển của SimpleArray
, có nhiệm vụ đóng lỗ hổng này. Để làm điều này, hãy thêm một phương thức mẫu khác vào lớp với một nạp chồng của toán tử <<
– lần này dành cho các con trỏ. Vì nó là một mẫu, nó cũng chỉ được bao gồm trong mã nguồn kết quả "theo yêu cầu": khi lập trình viên cố gắng sử dụng nạp chồng này, tức là ghi một con trỏ vào chứa. Nếu không, phương thức sẽ bị bỏ qua.
template<typename T>
class SimpleArray
{
...
template<typename P>
SimpleArray *operator<<(P *p)
{
data[expand()] = (T)*p;
if(CheckPointer(p) == POINTER_DYNAMIC) delete p;
return &this;
}
2
3
4
5
6
7
8
9
10
11
Chuyên biệt hóa này gây ra lỗi biên dịch ("object pointer expected") khi khởi tạo một mẫu với kiểu con trỏ. Do đó, chúng ta thông báo cho người dùng rằng chế độ này không được hỗ trợ.
SimpleArray<Dummy*> bad; // LỖI được tạo ra trong SimpleArray.mqh
Ngoài ra, nó thực hiện một hành động bảo vệ khác. Nếu lớp khách vẫn có hàm tạo sao chép, thì việc lưu trữ các đối tượng được phân bổ động trong chứa sẽ không còn dẫn đến rò rỉ bộ nhớ: một bản sao của đối tượng tại con trỏ được truyền P *p
vẫn còn trong chứa, và bản gốc bị xóa. Khi chứa bị hủy tại cuối hàm OnStart
, mảng nội bộ data
của nó sẽ tự động gọi các hàm hủy cho các phần tử của nó.
void OnStart()
{
...
SimpleArray<Dummy> good;
good << new Dummy(0);
} // SimpleArray "dọn dẹp" các phần tử của nó
// không có đối tượng bị quên trong bộ nhớ
2
3
4
5
6
7
Các phương thức mẫu và các phương thức "đơn giản" có thể được định nghĩa bên ngoài khối lớp chính (hoặc mẫu lớp), tương tự như những gì chúng ta đã thấy trong phần Tách khai báo và định nghĩa của lớp. Đồng thời, tất cả đều được đặt trước bởi tiêu đề mẫu (TemplatesExtended.mq5
):
template<typename T>
class ClassType
{
ClassType() // hàm tạo riêng
{
s = &this;
}
static ClassType *s; // con trỏ đối tượng (nếu nó đã được tạo)
public:
static ClassType *create() // tạo (chỉ trên lần gọi đầu tiên)
{
static ClassType single; // mẫu singleton cho mỗi T
return single;
}
static ClassType *check() // kiểm tra con trỏ mà không tạo
{
return s;
}
template<typename U>
void method(const U &u);
};
template<typename T>
template<typename U>
void ClassType::method(const U &u)
{
Print(__FUNCSIG__, " ", typename(T), " ", typename(U));
}
template<typename T>
static ClassType<T> *ClassType::s = NULL;
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
33
Nó cũng cho thấy việc khởi tạo một biến tĩnh mẫu, biểu thị mẫu thiết kế singleton.
Trong hàm OnStart
, tạo một phiên bản của mẫu và kiểm tra nó:
void OnStart()
{
ClassType<string> *object = ClassType<string>::create();
double d = 5.0;
object.method(d);
// ĐẦU RA:
// void ClassType<string>::method<double>(const double&) string double
Print(ClassType<string>::check()); // 1048576 (một ví dụ về id phiên bản)
Print(ClassType<long>::check()); // 0 (không có phiên bản cho T=long)
}
2
3
4
5
6
7
8
9
10
11