Có bao giờ có lý do chính đáng để không khai báo hàm hủy ảo cho một lớp không? Khi nào bạn nên tránh viết một cách cụ thể?
Có bao giờ có lý do chính đáng để không khai báo hàm hủy ảo cho một lớp không? Khi nào bạn nên tránh viết một cách cụ thể?
Câu trả lời:
Không cần sử dụng trình hủy ảo khi bất kỳ điều nào dưới đây là đúng:
Không có lý do cụ thể nào để tránh nó, trừ khi bạn thực sự quá ấn tượng với bộ nhớ.
Để trả lời câu hỏi một cách rõ ràng, tức là khi nào bạn không nên khai báo một hàm hủy ảo.
C ++ '98 / '03
Việc thêm trình hủy ảo có thể thay đổi lớp của bạn từ POD (dữ liệu cũ thuần túy) * hoặc tổng hợp thành không phải POD. Điều này có thể ngăn dự án của bạn biên dịch nếu loại lớp của bạn được khởi tạo tổng hợp ở đâu đó.
struct A {
// virtual ~A ();
int i;
int j;
};
void foo () {
A a = { 0, 1 }; // Will fail if virtual dtor declared
}
Trong trường hợp cực đoan, sự thay đổi như vậy cũng có thể gây ra hành vi không xác định trong đó lớp đang được sử dụng theo cách yêu cầu POD, ví dụ: truyền nó qua tham số dấu chấm lửng hoặc sử dụng nó với memcpy.
void bar (...);
void foo (A & a) {
bar (a); // Undefined behavior if virtual dtor declared
}
[* Kiểu POD là kiểu có các đảm bảo cụ thể về bố cục bộ nhớ của nó. Tiêu chuẩn thực sự chỉ nói rằng nếu bạn sao chép từ một đối tượng có kiểu POD vào một mảng ký tự (hoặc ký tự không dấu) và quay lại một lần nữa, thì kết quả sẽ giống như đối tượng ban đầu.]
C ++ hiện đại
Trong các phiên bản gần đây của C ++, khái niệm POD được phân tách giữa bố cục lớp và việc xây dựng, sao chép và phá hủy nó.
Đối với trường hợp dấu chấm lửng, nó không còn là hành vi không xác định nữa, nó hiện được hỗ trợ có điều kiện với ngữ nghĩa do triển khai xác định (N3937 - ~ C ++ '14 - 5.2.2 / 7):
... Truyền một đối số được đánh giá có khả năng thuộc loại lớp (Khoản 9) có hàm tạo sao chép không tầm thường, hàm tạo di chuyển không tầm thường hoặc hàm hủy tầm thường, không có tham số tương ứng, được hỗ trợ có điều kiện với việc triển khai- ngữ nghĩa xác định.
Khai báo một trình hủy khác =default
sẽ có nghĩa là nó không tầm thường (12,4 / 5)
... Một trình hủy là tầm thường nếu nó không do người dùng cung cấp ...
Các thay đổi khác đối với Modern C ++ làm giảm tác động của vấn đề khởi tạo tổng hợp vì có thể thêm một hàm tạo:
struct A {
A(int i, int j);
virtual ~A ();
int i;
int j;
};
void foo () {
A a = { 0, 1 }; // OK
}
Tôi khai báo một hàm hủy ảo nếu và chỉ khi tôi có các phương thức ảo. Khi tôi có các phương thức ảo, tôi không tin tưởng bản thân mình sẽ tránh khởi tạo nó trên heap hoặc lưu trữ một con trỏ đến lớp cơ sở. Cả hai thao tác này đều là những thao tác cực kỳ phổ biến và thường sẽ âm thầm làm rò rỉ tài nguyên nếu trình hủy không được khai báo là ảo.
Một trình hủy ảo là cần thiết bất cứ khi nào có bất kỳ cơ hội nào delete
có thể được gọi trên một con trỏ tới một đối tượng của một lớp con với kiểu lớp của bạn. Điều này đảm bảo rằng trình hủy chính xác được gọi tại thời điểm chạy mà trình biên dịch không cần biết lớp của một đối tượng trên heap tại thời điểm biên dịch. Ví dụ, giả sử B
là một lớp con của A
:
A *x = new B;
delete x; // ~B() called, even though x has type A*
Nếu mã của bạn không quan trọng về hiệu suất, sẽ là hợp lý nếu thêm một trình hủy ảo vào mọi lớp cơ sở mà bạn viết, chỉ để an toàn.
Tuy nhiên, nếu bạn thấy mình nhập delete
nhiều đối tượng trong một vòng lặp chặt chẽ, thì hiệu suất của việc gọi một hàm ảo (ngay cả một hàm trống) có thể đáng chú ý. Trình biên dịch thường không thể nội dòng các lệnh gọi này và bộ xử lý có thể gặp khó khăn trong việc dự đoán nơi sẽ đi. Điều này không chắc sẽ có tác động đáng kể đến hiệu suất, nhưng điều đáng nói là.
Các hàm ảo có nghĩa là mọi đối tượng được cấp phát đều tăng chi phí bộ nhớ bởi một con trỏ bảng hàm ảo.
Vì vậy, nếu chương trình của bạn liên quan đến việc phân bổ một số lượng rất lớn một số đối tượng, bạn nên tránh tất cả các hàm ảo để tiết kiệm 32 bit bổ sung cho mỗi đối tượng.
Trong tất cả các trường hợp khác, bạn sẽ tiết kiệm cho mình những khó khăn gỡ lỗi để làm cho dtor ảo.
Không phải tất cả các lớp C ++ đều thích hợp để sử dụng làm lớp cơ sở với tính đa hình động.
Nếu bạn muốn lớp của mình phù hợp với tính đa hình động, thì trình hủy của nó phải là ảo. Ngoài ra, bất kỳ phương thức nào mà một lớp con có thể hình dung muốn ghi đè (có thể có nghĩa là tất cả các phương thức công khai, cộng với một số phương thức được bảo vệ có khả năng được sử dụng nội bộ) phải là ảo.
Nếu lớp của bạn không phù hợp với tính đa hình động, thì hàm hủy không nên được đánh dấu là ảo, vì làm như vậy sẽ gây hiểu lầm. Nó chỉ khuyến khích mọi người sử dụng lớp học của bạn không đúng cách.
Đây là một ví dụ về một lớp sẽ không phù hợp với tính đa hình động, ngay cả khi trình hủy của nó là ảo:
class MutexLock {
mutex *mtx_;
public:
explicit MutexLock(mutex *mtx) : mtx_(mtx) { mtx_->lock(); }
~MutexLock() { mtx_->unlock(); }
private:
MutexLock(const MutexLock &rhs);
MutexLock &operator=(const MutexLock &rhs);
};
Toàn bộ điểm của lớp này là ngồi trên ngăn xếp cho RAII. Nếu bạn đang chuyển các con trỏ tới các đối tượng của lớp này, chứ chưa nói đến các lớp con của nó, thì bạn đang làm sai.
Một lý do chính đáng để không khai báo hàm hủy là ảo là khi điều này giúp lớp của bạn không bị thêm một bảng hàm ảo và bạn nên tránh điều đó bất cứ khi nào có thể.
Tôi biết rằng nhiều người thích luôn luôn khai báo các trình hủy là ảo, chỉ để an toàn. Nhưng nếu lớp của bạn không có bất kỳ hàm ảo nào khác thì việc có một hàm hủy ảo thực sự không có ích lợi gì. Ngay cả khi bạn cung cấp lớp của mình cho những người khác, những người sau đó lấy các lớp khác từ nó thì họ sẽ không có lý do gì để gọi xóa trên một con trỏ được truyền lên lớp của bạn - và nếu họ làm vậy thì tôi sẽ coi đây là một lỗi.
Được rồi, có một ngoại lệ duy nhất, cụ thể là nếu lớp của bạn (sai) được sử dụng để thực hiện xóa đa hình các đối tượng dẫn xuất, nhưng bạn - hoặc những người khác - hy vọng biết rằng điều này yêu cầu một trình hủy ảo.
Nói một cách khác, nếu lớp của bạn có hàm hủy không phải ảo thì đây là một tuyên bố rất rõ ràng: "Đừng sử dụng tôi để xóa các đối tượng dẫn xuất!"
Nếu bạn có một lớp rất nhỏ với số lượng lớn các phiên bản, chi phí của một con trỏ vtable có thể tạo ra sự khác biệt trong việc sử dụng bộ nhớ chương trình của bạn. Miễn là lớp của bạn không có bất kỳ phương thức ảo nào khác, việc làm cho hàm hủy không phải là ảo sẽ tiết kiệm chi phí đó.
Tôi thường khai báo hàm hủy ảo, nhưng nếu bạn có mã quan trọng về hiệu suất được sử dụng trong vòng lặp bên trong, bạn có thể muốn tránh tra cứu bảng ảo. Điều đó có thể quan trọng trong một số trường hợp, như kiểm tra va chạm. Nhưng hãy cẩn thận về cách bạn phá hủy các đối tượng đó nếu bạn sử dụng tính năng thừa kế, nếu không bạn sẽ chỉ phá hủy một nửa đối tượng.
Lưu ý rằng tra cứu bảng ảo xảy ra cho một đối tượng nếu bất kỳ phương thức nào trên đối tượng đó là ảo. Vì vậy, không ích gì khi loại bỏ đặc tả ảo trên một hàm hủy nếu bạn có các phương thức ảo khác trong lớp.
Nếu bạn hoàn toàn tích cực phải đảm bảo rằng lớp của bạn không có vtable thì bạn cũng không được có hàm hủy ảo.
Đây là một trường hợp hiếm, nhưng nó vẫn xảy ra.
Ví dụ quen thuộc nhất về mẫu thực hiện điều này là các lớp DirectX D3DVECTOR và D3DMATRIX. Đây là các phương thức của lớp thay vì các hàm cho đường cú pháp, nhưng các lớp cố ý không có vtable để tránh phí hàm vì các lớp này được sử dụng cụ thể trong vòng lặp bên trong của nhiều ứng dụng hiệu suất cao.
Trên hoạt động sẽ được thực hiện trên lớp cơ sở và hoạt động đó sẽ hoạt động ảo, phải là ảo. Nếu việc xóa có thể được thực hiện đa hình thông qua giao diện lớp cơ sở, thì nó phải hoạt động ảo và ảo.
Hàm hủy không cần phải ảo nếu bạn không có ý định dẫn xuất từ lớp. Và ngay cả khi bạn làm vậy, một trình hủy không ảo được bảo vệ cũng tốt nếu không cần xóa các con trỏ lớp cơ sở .
Câu trả lời về hiệu suất là câu trả lời duy nhất tôi biết có cơ hội thành sự thật. Nếu bạn đã đo lường và nhận thấy rằng việc khử ảo các trình hủy của bạn thực sự tăng tốc mọi thứ, thì bạn có thể có những thứ khác trong lớp đó cũng cần tăng tốc, nhưng tại thời điểm này, có nhiều cân nhắc quan trọng hơn. Một ngày nào đó ai đó sẽ phát hiện ra rằng mã của bạn sẽ cung cấp một lớp cơ sở tốt cho họ và tiết kiệm công việc của họ trong một tuần. Tốt hơn bạn nên đảm bảo rằng họ thực hiện công việc của tuần đó, sao chép và dán mã của bạn, thay vì sử dụng mã của bạn làm cơ sở. Tốt hơn bạn nên đảm bảo rằng bạn đặt một số phương thức quan trọng của mình ở chế độ riêng tư để không ai có thể kế thừa từ bạn.