Ép kiểu loại đối tượng: dynamic_cast
và con trỏ void *
Các loại đối tượng có các quy tắc ép kiểu cụ thể được áp dụng khi kiểu của biến nguồn và biến đích không khớp. Quy tắc cho các kiểu tích hợp sẵn đã được thảo luận trong Chương 2.6 Chuyển đổi kiểu. Đặc điểm của việc ép kiểu cấu trúc khi sao chép đã được mô tả trong phần Bố cục và kế thừa cấu trúc.
Đối với cả cấu trúc và lớp, điều kiện chính để việc ép kiểu được chấp nhận là chúng phải có quan hệ trong chuỗi kế thừa. Các kiểu từ các nhánh khác nhau của hệ thống phân cấp hoặc không liên quan gì đến nhau thì không thể ép kiểu cho nhau được.
Quy tắc ép kiểu khác nhau giữa đối tượng (giá trị) và con trỏ.
Đối tượng
Một đối tượng của kiểu A có thể được gán cho một đối tượng của kiểu B nếu kiểu B có một hàm tạo nhận tham số kiểu A (có thể theo giá trị, tham chiếu hoặc con trỏ, nhưng thường có dạng B(const A &a)
). Hàm tạo như vậy còn được gọi là hàm tạo chuyển đổi.
Nếu không có hàm tạo rõ ràng như vậy, trình biên dịch sẽ cố gắng sử dụng toán tử sao chép ngầm định, tức là B::operator=(const B &b)
, trong khi các lớp A và B phải nằm trong cùng một chuỗi kế thừa để việc sao chép ngầm định từ A sang B hoạt động. Nếu A được kế thừa từ B (bao gồm cả không trực tiếp mà gián tiếp), thì các thuộc tính được thêm vào A sẽ biến mất khi sao chép sang B. Nếu B được kế thừa từ A, thì chỉ phần thuộc tính có trong A sẽ được sao chép vào B. Những chuyển đổi như vậy thường không được khuyến khích.
Ngoài ra, toán tử sao chép ngầm định không phải lúc nào cũng được trình biên dịch cung cấp. Đặc biệt, nếu lớp có các trường với bộ sửa đổi const
, việc sao chép bị coi là bị cấm (xem thêm ở phần sau).
Trong tập lệnh ShapesCasting.mq5
, chúng ta sử dụng hệ thống phân cấp lớp hình học để thể hiện việc chuyển đổi kiểu đối tượng. Trong lớp Shape
, trường type
được cố ý đặt là hằng số, vì vậy nỗ lực chuyển đổi (gán) một đối tượng Square
sang một đối tượng Rectangle
sẽ kết thúc bằng lỗi biên dịch với giải thích chi tiết:
attempting to reference deleted function 'void Rectangle::operator=(const Rectangle&)'
function 'void Rectangle::operator=(const Rectangle&)' was implicitly deleted
because it invokes deleted function 'void Shape::operator=(const Shape&)'
function 'void Shape::operator=(const Shape&)' was implicitly deleted
because member 'type' has 'const' modifier
2
3
4
5
Theo thông báo này, phương thức sao chép Rectangle::operator=(const Rectangle&)
đã bị trình biên dịch tự động xóa (mặc dù trình biên dịch cung cấp triển khai mặc định của nó) vì nó sử dụng một phương thức tương tự trong lớp cơ sở Shape::operator=(const Shape&)
, mà phương thức này lại bị xóa do sự hiện diện của trường type
với bộ sửa đổi const
. Các trường như vậy chỉ có thể được đặt giá trị khi đối tượng được tạo, và trình biên dịch không biết cách sao chép đối tượng dưới sự hạn chế này.
Nhân tiện, hiệu ứng "xóa" phương thức không chỉ có sẵn cho trình biên dịch mà còn cho lập trình viên ứng dụng: chi tiết hơn sẽ được thảo luận trong phần Kiểm soát kế thừa: final và delete.
Vấn đề có thể được giải quyết bằng cách loại bỏ bộ sửa đổi const
hoặc cung cấp triển khai riêng của toán tử gán (trong đó, trường const
không tham gia và sẽ giữ nội dung mô tả loại: "Rectangle"):
Rectangle *operator=(const Rectangle &r)
{
coordinates.x = r.coordinates.x;
coordinates.y = r.coordinates.y;
backgroundColor = r.backgroundColor;
dx = r.dx;
dy = r.dy;
return &this;
}
2
3
4
5
6
7
8
9
Lưu ý rằng định nghĩa này trả về một con trỏ đến đối tượng hiện tại, trong khi triển khai mặc định do trình biên dịch tạo ra có kiểu void
(như được thấy trong thông báo lỗi). Điều này có nghĩa là các toán tử gán mặc định do trình biên dịch cung cấp không thể được sử dụng trong chuỗi x = y = z
. Nếu bạn cần khả năng này, hãy ghi đè rõ ràng operator=
và trả về kiểu mong muốn khác với void
.
Con trỏ
Cách thực tế nhất là chuyển đổi con trỏ sang các đối tượng của các kiểu khác nhau.
Về lý thuyết, tất cả các tùy chọn để ép kiểu con trỏ loại đối tượng có thể được rút gọn thành ba trường hợp:
- Từ cơ sở đến dẫn xuất, ép kiểu xuống dưới (downcast), vì thông thường hệ thống phân cấp lớp được vẽ dưới dạng cây ngược;
- Từ dẫn xuất đến cơ sở, ép kiểu lên trên (upcast);
- Giữa các lớp của các nhánh khác nhau trong hệ thống phân cấp hoặc thậm chí từ các họ khác nhau.
Trường hợp cuối cùng bị cấm (chúng ta sẽ nhận được lỗi biên dịch). Trình biên dịch cho phép hai trường hợp đầu tiên, nhưng nếu "upcast" là tự nhiên và an toàn, thì "downcast" có thể dẫn đến lỗi thời gian chạy.
void OnStart()
{
Rectangle *r = addRandomShape(Shape::SHAPES::RECTANGLE);
Square *s = addRandomShape(Shape::SHAPES::SQUARE);
Circle *c = NULL;
Shape *p;
Rectangle *r2;
// Được phép
p = c; // Hình tròn -> Hình dạng
p = s; // Hình vuông -> Hình dạng
p = r; // Hình chữ nhật -> Hình dạng
r2 = p; // Hình dạng -> Hình chữ nhật
...
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Tất nhiên, khi một con trỏ đến đối tượng của lớp cơ sở được sử dụng, các phương thức và thuộc tính của lớp dẫn xuất không thể được gọi trên nó, ngay cả khi đối tượng tương ứng nằm tại con trỏ đó. Chúng ta sẽ nhận được lỗi biên dịch "undeclared identifier" (định danh chưa được khai báo).
Tuy nhiên, cú pháp ép kiểu rõ ràng được hỗ trợ cho con trỏ (xem kiểu C), cho phép chuyển đổi "on the fly" một con trỏ sang kiểu cần thiết trong biểu thức và giải tham chiếu nó mà không cần tạo biến trung gian.
Base *b;
Derived d;
b = &d;
((Derived *)b).derivedMethod();
2
3
4
Ở đây, chúng ta đã tạo một đối tượng lớp dẫn xuất (Derived
) và một con trỏ kiểu cơ sở đến nó (Base *
). Để truy cập phương thức derivedMethod
của lớp dẫn xuất, con trỏ được tạm thời chuyển đổi sang kiểu Derived
.
Kiểu con trỏ có dấu sao phải được đặt trong dấu ngoặc đơn. Ngoài ra, biểu thức ép kiểu, bao gồm cả tên biến, cũng được bao quanh bởi một cặp dấu ngoặc khác.
Một lỗi biên dịch khác ("type mismatch" - "không khớp kiểu") trong thử nghiệm của chúng ta xuất hiện khi chúng ta cố gắng ép kiểu một con trỏ sang Rectangle
thành một con trỏ sang Circle
: chúng thuộc các nhánh kế thừa khác nhau.
c = r; // lỗi: không khớp kiểu
Mọi thứ trở nên tồi tệ hơn khi kiểu của con trỏ được ép kiểu không khớp với đối tượng thực tế (mặc dù các kiểu của chúng tương thích, và do đó chương trình biên dịch tốt). Thao tác như vậy sẽ kết thúc bằng lỗi ở giai đoạn thực thi chương trình (tức là trình biên dịch không thể phát hiện ra). Sau đó, chương trình bị gỡ bỏ.
Ví dụ, trong tập lệnh ShapesCasting.mq5
, chúng ta đã mô tả một con trỏ đến Square
và gán cho nó một con trỏ đến Shape
, mà chứa đối tượng Rectangle
.
Square *s2;
// LỖI THỜI GIAN CHẠY
s2 = p; // lỗi: Ép kiểu con trỏ không chính xác
2
3
Thiết bị đầu cuối trả về lỗi "Incorrect casting of pointers" (Ép kiểu con trỏ không chính xác). Con trỏ của kiểu cụ thể hơn Square
không thể trỏ đến đối tượng cha Rectangle
.
Để tránh các rắc rối thời gian chạy và ngăn chương trình bị sập, MQL5 cung cấp một cấu trúc ngôn ngữ đặc biệt dynamic_cast
. Với cấu trúc này, bạn có thể "cẩn thận" kiểm tra xem có thể ép kiểu một con trỏ sang kiểu cần thiết hay không. Nếu chuyển đổi khả thi, nó sẽ được thực hiện. Nếu không, chúng ta sẽ nhận được một con trỏ rỗng (NULL) và có thể xử lý nó theo cách đặc biệt (ví dụ, sử dụng if
để khởi tạo hoặc ngắt thực thi hàm, nhưng không phải toàn bộ chương trình).
Cú pháp của dynamic_cast
như sau:
dynamic_cast<Class *>(pointer)
Trong trường hợp của chúng ta, chỉ cần viết:
s2 = dynamic_cast<Square *>(p); // cố gắng ép kiểu, và sẽ nhận được NULL nếu không thành công
Print(s2); // 0
2
Chương trình sẽ chạy như kỳ vọng.
Cụ thể, chúng ta có thể thử lại việc ép kiểu một hình chữ nhật thành hình tròn và đảm bảo rằng chúng ta nhận được 0:
c = dynamic_cast<Circle *>(r); // cố gắng ép kiểu, và sẽ nhận được NULL nếu không thành công
Print(c); // 0
2
Trong MQL5, có một kiểu con trỏ đặc biệt có thể lưu trữ bất kỳ đối tượng nào. Kiểu này được ký hiệu là: void *
.
Hãy thể hiện cách biến void *
hoạt động với dynamic_cast
.
void *v;
v = s; // đặt thành thể hiện Hình vuông
PRT(dynamic_cast<Shape *>(v));
PRT(dynamic_cast<Rectangle *>(v));
PRT(dynamic_cast<Square *>(v));
PRT(dynamic_cast<Circle *>(v));
PRT(dynamic_cast<Triangle *>(v));
2
3
4
5
6
7
Ba dòng đầu tiên sẽ ghi lại giá trị của con trỏ (mô tả của cùng một đối tượng), và hai dòng cuối sẽ in ra 0.
Bây giờ, quay lại ví dụ về khai báo trước trong phần Chỉ số (xem tệp ThisCallback.mq5
), nơi các lớp Manager
và Element
chứa các con trỏ lẫn nhau.
Kiểu con trỏ void *
cho phép loại bỏ khai báo trước (ThisCallbackVoid.mq5
). Hãy bình luận dòng có nó, và thay đổi kiểu của trường owner
với con trỏ đến đối tượng quản lý thành void *
. Trong hàm tạo, chúng ta cũng thay đổi kiểu của tham số.
// class Manager; // bình luận: lớp Manager;
class Element
{
void *owner; // mong muốn tương thích với kiểu Manager *
public:
Element(void *t = NULL): owner(t) { } // trước đây là Element(Manager &t)
void doMath()
{
const int N = 1000000;
// lấy kiểu mong muốn tại thời gian chạy
Manager *ptr = dynamic_cast<Manager *>(owner);
// sau đó ở mọi nơi cần kiểm tra ptr có phải NULL trước khi sử dụng không
for(int i = 0; i < N; ++i)
{
if(i % (N / 20) == 0)
{
if(ptr != NULL) ptr.progressNotify(&this, i * 100.0f / N);
}
// ... rất nhiều phép tính
}
if(ptr != NULL) ptr.progressNotify(&this, 100.0f);
}
...
};
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
Cách tiếp cận này có thể cung cấp nhiều linh hoạt hơn nhưng đòi hỏi cẩn thận hơn vì dynamic_cast
có thể trả về NULL. Nên sử dụng các phương tiện phân phối tiêu chuẩn (tĩnh và động) với việc kiểm soát các kiểu do ngôn ngữ cung cấp bất cứ khi nào có thể.
Con trỏ void *
thường trở nên cần thiết trong các trường hợp đặc biệt. Và dòng "thừa" với mô tả sơ bộ không phải là trường hợp đó. Nó đã được sử dụng ở đây chỉ như ví dụ đơn giản nhất về tính phổ quát của con trỏ void *
.