Kiểu đối tượng templates
Định nghĩa mẫu kiểu đối tượng bắt đầu bằng một tiêu đề chứa các tham số kiểu (xem phần Tiêu đề mẫu), và định nghĩa thông thường của một lớp, cấu trúc hoặc liên minh.
template<typename T [, typename Ti ...] >
class class_name
{
...
};
2
3
4
5
Điểm khác biệt duy nhất so với định nghĩa tiêu chuẩn là các tham số mẫu có thể xuất hiện trong khối mã, trong tất cả các cấu trúc cú pháp của ngôn ngữ, nơi việc sử dụng tên kiểu là hợp lệ.
Sau khi một mẫu được định nghĩa, các phiên bản hoạt động của nó được tạo ra khi các biến của kiểu mẫu được khai báo trong mã, chỉ định các kiểu cụ thể trong dấu ngoặc nhọn:
ClassName<Type1,Type2> object;
StructName<Type1,Type2,Type3> struct;
ClassName<Type1,Type2> *pointer = new ClassName<Type1,Type2>();
ClassName1<ClassName2<Type>> object;
2
3
4
Không giống như khi gọi các hàm mẫu, trình biên dịch không thể tự suy ra các kiểu thực tế cho các mẫu đối tượng.
Khai báo một biến lớp/cấu trúc mẫu không phải là cách duy nhất để khởi tạo một mẫu. Một phiên bản cũng được trình biên dịch tạo ra nếu một kiểu mẫu được sử dụng làm kiểu cơ sở cho một lớp hoặc cấu trúc cụ thể khác (không phải mẫu).
Ví dụ, lớp sau Worker
, ngay cả khi trống, là một triển khai của Base
cho kiểu double
:
class Worker : Base<double>
{
};
2
3
Định nghĩa tối thiểu này là đủ (với điều kiện thêm các hàm tạo nếu lớp Base
yêu cầu) để bắt đầu biên dịch và xác thực mã mẫu.
Trong phần Tạo đối tượng động, chúng ta đã làm quen với khái niệm con trỏ động đến một đối tượng thu được bằng toán tử new
. Cơ chế linh hoạt này có một nhược điểm: các con trỏ cần được theo dõi và "xóa thủ công" khi chúng không còn cần thiết. Đặc biệt, khi thoát khỏi một hàm hoặc khối mã, tất cả các con trỏ cục bộ phải được xóa bằng lời gọi delete
.
Để đơn giản hóa việc giải quyết vấn đề này, hãy tạo một lớp mẫu AutoPtr
(TemplatesAutoPtr.mq5
, AutoPtr.mqh
). Tham số T của nó được sử dụng để mô tả trường ptr
, lưu trữ con trỏ đến một đối tượng của một lớp bất kỳ. Chúng ta sẽ nhận giá trị con trỏ thông qua tham số hàm tạo (T *p
) hoặc trong toán tử '=' được nạp chồng. Hãy giao phó công việc chính cho hàm hủy: trong hàm hủy, con trỏ sẽ bị xóa cùng với đối tượng AutoPtr
(phương thức trợ giúp tĩnh free
được phân bổ cho việc này).
Nguyên tắc hoạt động của AutoPtr
rất đơn giản: một đối tượng cục bộ của lớp này sẽ tự động bị hủy khi thoát khỏi khối nơi nó được mô tả, và nếu trước đó nó được chỉ định "theo dõi" một con trỏ nào đó, thì AutoPtr
cũng sẽ giải phóng nó.
template<typename T>
class AutoPtr
{
private:
T *ptr;
public:
AutoPtr() : ptr(NULL) { }
AutoPtr(T *p) : ptr(p)
{
Print(__FUNCSIG__, " ", &this, ": ", ptr);
}
AutoPtr(AutoPtr &p)
{
Print(__FUNCSIG__, " ", &this, ": ", ptr, " -> ", p.ptr);
free(ptr);
ptr = p.ptr;
p.ptr = NULL;
}
~AutoPtr()
{
Print(__FUNCSIG__, " ", &this, ": ", ptr);
free(ptr);
}
T *operator=(T *n)
{
Print(__FUNCSIG__, " ", &this, ": ", ptr, " -> ", n);
free(ptr);
ptr = n;
return ptr;
}
T* operator[](int x = 0) const
{
return ptr;
}
static void free(void *p)
{
if(CheckPointer(p) == POINTER_DYNAMIC) delete p;
}
};
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
34
35
36
37
38
39
40
41
42
43
44
45
46
Ngoài ra, lớp AutoPtr
triển khai một hàm tạo sao chép (chính xác hơn là một hàm tạo nhảy, vì đối tượng hiện tại trở thành chủ sở hữu của con trỏ), cho phép trả về một phiên bản AutoPtr
cùng với con trỏ được kiểm soát từ một hàm.
Để kiểm tra hiệu suất của AutoPtr
, chúng ta sẽ mô tả một lớp giả lập Dummy
.
class Dummy
{
int x;
public:
Dummy(int i) : x(i)
{
Print(__FUNCSIG__, " ", &this);
}
...
int value() const
{
return x;
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
Trong script, trong hàm OnStart
, nhập biến AutoPtr<Dummy>
và lấy giá trị cho nó từ hàm generator
. Trong chính hàm generator
, chúng ta cũng sẽ mô tả đối tượng AutoPtr<Dummy>
và lần lượt tạo và "gắn" hai đối tượng động Dummy
vào nó (để kiểm tra việc giải phóng bộ nhớ đúng cách từ đối tượng "cũ").
AutoPtr<Dummy> generator()
{
AutoPtr<Dummy> ptr(new Dummy(1));
// con trỏ tới 1 sẽ được giải phóng sau khi thực thi '='
ptr = new Dummy(2);
return ptr;
}
void OnStart()
{
AutoPtr<Dummy> ptr = generator();
Print(ptr[].value()); // 2
}
2
3
4
5
6
7
8
9
10
11
12
13
Vì tất cả các phương thức chính đều ghi lại các mô tả đối tượng (cả AutoPtr
và con trỏ được kiểm soát ptr
), chúng ta có thể theo dõi tất cả các "biến đổi" của con trỏ (để tiện lợi, tất cả các dòng được đánh số).
01 Dummy::Dummy(int) 3145728
02 AutoPtr<Dummy>::AutoPtr<Dummy>(Dummy*) 2097152: 3145728
03 Dummy::Dummy(int) 4194304
04 Dummy*AutoPtr<Dummy>::operator=(Dummy*) 2097152: 3145728 -> 4194304
05 Dummy::~Dummy() 3145728
06 AutoPtr<Dummy>::AutoPtr<Dummy>(AutoPtr<Dummy>&) 5242880: 0 -> 4194304
07 AutoPtr<Dummy>::~AutoPtr<Dummy>() 2097152: 0
08 AutoPtr<Dummy>::AutoPtr<Dummy>(AutoPtr<Dummy>&) 1048576: 0 -> 4194304
09 AutoPtr<Dummy>::~AutoPtr<Dummy>() 5242880: 0
10 2
11 AutoPtr<Dummy>::~AutoPtr<Dummy>() 1048576: 4194304
12 Dummy::~Dummy() 4194304
2
3
4
5
6
7
8
9
10
11
12
Hãy tạm rời khỏi các mẫu và mô tả chi tiết cách hoạt động của tiện ích này vì một lớp như vậy có thể hữu ích cho nhiều người.
Ngay sau khi bắt đầu
OnStart
, hàmgenerator
được gọi. Nó phải trả về một giá trị để khởi tạo đối tượngAutoPtr
trongOnStart
, và do đó hàm tạo của nó chưa được gọi. Dòng 02 tạo một đối tượngAutoPtr#2097152
bên trong hàmgenerator
và nhận được con trỏ tớiDummy#3145728
đầu tiên. Tiếp theo, một phiên bản thứ hai củaDummy#4194304
được tạo (dòng 03), thay thế bản sao trước đó với mô tả 3145728 (dòng 04) trongAutoPtr#2097152
, và bản sao cũ bị xóa (dòng 05). Dòng 06 tạo mộtAutoPtr#5242880
tạm thời để trả về giá trị từgenerator
, và xóa cái cục bộ (07). Ở dòng 08, hàm tạo sao chép cho đối tượngAutoPtr#1048576
trong hàmOnStart
cuối cùng được sử dụng, và con trỏ từ đối tượng tạm thời (được xóa ngay lập tức ở dòng 09) được chuyển sang nó. Tiếp theo, chúng ta gọiOnStart
hoàn tất, hàm hủyAutoPtr
(11) tự động kích hoạt, khiến chúng ta cũng xóa đối tượng làm việcDummy
(12).
Công nghệ mẫu biến lớp AutoPtr
thành một trình quản lý tham số hóa của các đối tượng được phân bổ động. Nhưng vì AutoPtr
có trường T *ptr
, nó chỉ áp dụng cho các lớp (chính xác hơn là con trỏ tới các đối tượng lớp). Ví dụ, việc cố gắng khởi tạo một mẫu cho chuỗi (AutoPtr<string> s
) sẽ dẫn đến nhiều lỗi trong văn bản mẫu, ý nghĩa là kiểu string
không hỗ trợ con trỏ.
Điều này không phải là vấn đề ở đây, vì mục đích của mẫu này giới hạn ở các lớp, nhưng đối với các mẫu tổng quát hơn, sắc thái này cần được ghi nhớ (xem phần ghi chú).
Con trỏ và tham chiếu
Hãy lưu ý rằng cấu trúc
T *
không thể xuất hiện trong các mẫu mà bạn dự định sử dụng, bao gồm cả cho các kiểu tích hợp hoặc cấu trúc. Vấn đề là trong MQL5, con trỏ chỉ được phép cho các lớp. Không phải là không thể viết một mẫu áp dụng cho cả kiểu tích hợp và kiểu do người dùng định nghĩa, nhưng nó có thể đòi hỏi một số điều chỉnh. Có lẽ sẽ cần phải từ bỏ một số chức năng hoặc hy sinh mức độ tổng quát của mẫu (tạo nhiều mẫu thay vì một, nạp chồng hàm, v.v.).Cách đơn giản nhất để "chèn" một kiểu con trỏ vào mẫu là bao gồm bộ sửa đổi '*' cùng với kiểu thực tế khi mẫu được khởi tạo (tức là nó phải khớp
T=Type*
). Tuy nhiên, một số hàm (nhưCheckPointer
), toán tử (nhưdelete
), và cấu trúc cú pháp (như ép kiểu((T)variable)
) nhạy cảm với việc các đối số/toán hạng của chúng có phải là con trỏ hay không. Vì lý do này, cùng một văn bản mẫu không phải lúc nào cũng đúng cú pháp cho cả con trỏ và giá trị kiểu đơn giản.Một sự khác biệt lớn khác về kiểu cần ghi nhớ: các đối tượng chỉ được truyền vào các phương thức qua tham chiếu, nhưng các hằng số (literals) của kiểu đơn giản không thể được truyền qua tham chiếu. Do đó, sự hiện diện hoặc vắng mặt của dấu & có thể được trình biên dịch coi là lỗi, tùy thuộc vào kiểu T được suy ra. Là một trong những "giải pháp", bạn có thể tùy chọn "bao bọc" các hằng số đối số vào các đối tượng hoặc biến.
Một mẹo khác liên quan đến việc sử dụng các phương thức mẫu. Chúng ta sẽ thấy điều này trong phần tiếp theo.
Cần lưu ý rằng các kỹ thuật hướng đối tượng kết hợp tốt với các mẫu. Vì một con trỏ tới lớp cơ sở có thể được sử dụng để lưu trữ một đối tượng của lớp dẫn xuất, AutoPtr
áp dụng được cho các đối tượng của bất kỳ lớp dẫn xuất nào của Dummy
.
Về lý thuyết, cách tiếp cận "lai" này được sử dụng rộng rãi trong các lớp chứa (vector, queue, map, list, v.v.), vốn thường là các mẫu. Các lớp chứa, tùy thuộc vào triển khai, có thể áp đặt các yêu cầu bổ sung lên tham số mẫu, đặc biệt là kiểu nội tuyến phải có hàm tạo sao chép và toán tử gán (sao chép).
Thư viện chuẩn MQL5 được cung cấp cùng MetaTrader 5 chứa nhiều mẫu sẵn có từ loạt này: Stack.mqh
, Queue.mqh
, HashMap.mqh
, LinkedList.mqh
, RedBlackTree.mqh
, và các mẫu khác. Tất cả đều nằm trong thư mục MQL5/Include/Generic. Tuy nhiên, chúng không cung cấp khả năng kiểm soát các đối tượng động (con trỏ).
Chúng ta sẽ xem xét ví dụ của riêng mình về một lớp chứa đơn giản trong Mẫu phương thức.