Khi nào bạn không nên sử dụng hàm hủy ảo?


Câu trả lời:


72

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ó ý định lấy các lớp từ nó
  • Không có khởi tạo trên heap
  • Không có ý định lưu trữ trong một con trỏ của lớp cha

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ớ.


25
Đây không phải là một câu trả lời hay. "Không cần" khác với "không nên", và "không có ý định" khác với "không thể thực hiện được".
Lập trình viên Windows

5
Cũng nói thêm: không có ý định xóa một cá thể thông qua con trỏ lớp cơ sở.
Adam Rosenfield

9
Điều này không thực sự trả lời câu hỏi. Đâu là lý do chính đáng của bạn để không sử dụng một dtor ảo?
mxcl

9
Tôi nghĩ rằng khi không cần thiết phải làm điều gì đó, đó là lý do chính đáng để không làm điều đó. Nó tuân theo nguyên tắc thiết kế đơn giản của XP.
tháng

12
Bằng cách nói rằng bạn "không có ý định", bạn đang đặt ra một giả định rất lớn về cách lớp học của bạn sẽ được sử dụng. Đối với tôi, có vẻ như giải pháp đơn giản nhất trong hầu hết các trường hợp (do đó nên là mặc định) nên có các trình hủy ảo và chỉ tránh chúng nếu bạn có lý do cụ thể để không làm như vậy. Vì vậy, tôi vẫn tò mò về một lý do chính đáng.
ckarras

68

Để 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 =defaultsẽ 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
}

1
Bạn đúng, và tôi đã sai, hiệu suất không phải là lý do duy nhất. Nhưng điều này cho thấy tôi đã đúng về phần còn lại của nó: lập trình viên của lớp tốt hơn nên bao gồm mã để ngăn lớp đó bị kế thừa bởi bất kỳ ai khác.
Lập trình viên Windows

Richard thân mến, bạn có thể vui lòng bình luận thêm một chút về những gì bạn đã viết. Tôi không hiểu quan điểm của bạn, nhưng có vẻ như đó là điểm có giá trị duy nhất mà tôi đã tìm thấy bằng googling) Hoặc bạn có thể cho một liên kết đến một lời giải thích chi tiết hơn?
John Smith

1
@JohnSmith Tôi đã cập nhật câu trả lời. Hy vọng rằng điều này sẽ giúp.
Richard Corden

27

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.


3
Và, trên thực tế, có một tùy chọn cảnh báo trên gcc để cảnh báo chính xác trường hợp đó (các phương thức ảo nhưng không có dtor ảo).
CesarB

6
Sau đó, bạn không có nguy cơ bị rò rỉ bộ nhớ nếu bạn dẫn xuất từ ​​lớp, bất kể bạn có các hàm ảo khác hay không?
Mag Roader

1
Tôi đồng ý với mag. Việc sử dụng hàm hủy ảo và hoặc phương thức ảo là các yêu cầu riêng biệt. Bộ hủy ảo cung cấp khả năng cho một lớp thực hiện dọn dẹp (ví dụ: xóa bộ nhớ, đóng tệp, v.v.) VÀ cũng đảm bảo các hàm tạo của tất cả các thành viên của nó được gọi.
user48956

7

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 deletecó 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ử Blà 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 deletenhiề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à.


"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." nên được nhấn mạnh nhiều hơn trong mọi câu trả lời tôi thấy
csguy

5

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.


1
Chỉ cần nit-chọn, nhưng những ngày này một con trỏ thường sẽ là 64 bit thay vì 32.
Trưởng Geek

5

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.


2
Sử dụng đa hình không ngụ ý xóa đa hình. Có rất nhiều trường hợp sử dụng cho một lớp có các phương thức ảo nhưng không có hàm hủy ảo. Hãy xem xét một hộp thoại được định nghĩa tĩnh điển hình, trong khá nhiều bộ công cụ GUI. Cửa sổ mẹ sẽ hủy các đối tượng con và nó biết loại chính xác của từng đối tượng, tuy nhiên tất cả các cửa sổ con cũng sẽ được sử dụng đa hình ở bất kỳ vị trí nào, chẳng hạn như thử nghiệm lần truy cập, bản vẽ, API trợ năng tìm nạp văn bản cho văn bản- công cụ chuyển thành giọng nói, v.v.
Ben Voigt

4
Đúng, nhưng người hỏi đang hỏi khi nào bạn nên đặc biệt tránh trình hủy ảo. Đối với hộp thoại bạn mô tả, một bộ hủy ảo là vô nghĩa, nhưng IMO không có hại. Tôi không chắc mình sẽ tự tin rằng mình sẽ không bao giờ cần xóa hộp thoại bằng con trỏ lớp cơ sở - ví dụ: trong tương lai, tôi có thể muốn cửa sổ mẹ của mình tạo các đối tượng con của nó bằng cách sử dụng các nhà máy. Vì vậy, vấn đề không phải là tránh trình hủy ảo, chỉ là bạn có thể không bận tâm đến việc có nó. Tuy nhiên, một hàm hủy ảo trên một lớp không phù hợp để dẫn xuất sẽ có hại vì nó gây hiểu lầm.
Steve Jessop

4

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!"


3

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í đó.


1

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.


1

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.


0

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ở .


-7

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.


Tính đa hình chắc chắn sẽ làm mọi thứ chậm lại. So sánh nó với một tình huống mà chúng ta cần đa hình và chọn không, nó sẽ thậm chí còn chậm hơn. Ví dụ: chúng tôi triển khai tất cả logic tại trình hủy lớp cơ sở, sử dụng RTTI và một câu lệnh switch để dọn dẹp tài nguyên.
tháng

1
Trong C ++, bạn không có trách nhiệm ngăn tôi kế thừa từ các lớp của bạn mà bạn đã ghi chép là không phù hợp để sử dụng làm lớp cơ sở. Tôi có trách nhiệm sử dụng tài sản thừa kế một cách thận trọng. Tất nhiên, trừ khi hướng dẫn phong cách ngôi nhà nói khác.
Steve Jessop

1
... chỉ làm cho hàm hủy ảo không có nghĩa là lớp đó nhất thiết phải hoạt động chính xác như một lớp cơ sở. Vì vậy, đánh dấu nó ảo "chỉ vì", thay vì thực hiện đánh giá đó, là viết séc, mã của tôi không thể rút tiền.
Steve Jessop
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.