Chi phí sử dụng các công cụ hủy ảo là bao nhiêu nếu tôi sử dụng nó ngay cả khi không cần thiết?
Chi phí giới thiệu bất kỳ hàm ảo nào cho một lớp (được kế thừa hoặc một phần của định nghĩa lớp) là chi phí ban đầu rất cao (hoặc không phụ thuộc vào đối tượng) của một con trỏ ảo được lưu trữ trên mỗi đối tượng, như vậy:
struct Integer
{
virtual ~Integer() {}
int value;
};
Trong trường hợp này, chi phí bộ nhớ là tương đối lớn. Kích thước bộ nhớ thực của một thể hiện lớp bây giờ sẽ thường trông như thế này trên các kiến trúc 64 bit:
struct Integer
{
// 8 byte vptr overhead
int value; // 4 bytes
// typically 4 more bytes of padding for alignment of vptr
};
Tổng cộng là 16 byte cho Integer
lớp này, trái ngược với chỉ 4 byte. Nếu chúng tôi lưu trữ một triệu trong số này trong một mảng, chúng tôi sẽ sử dụng 16 megabyte bộ nhớ: gấp đôi kích thước của bộ đệm CPU L3 8 MB thông thường và lặp đi lặp lại qua một mảng như vậy có thể chậm hơn nhiều lần so với tương đương 4 megabyte không có con trỏ ảo là kết quả của lỗi bộ nhớ cache bổ sung và lỗi trang.
Tuy nhiên, chi phí con trỏ ảo này cho mỗi đối tượng không tăng với nhiều chức năng ảo hơn. Bạn có thể có 100 hàm thành viên ảo trong một lớp và chi phí cho mỗi cá thể vẫn là một con trỏ ảo.
Con trỏ ảo thường là mối quan tâm ngay lập tức hơn từ quan điểm trên không. Tuy nhiên, ngoài một con trỏ ảo cho mỗi phiên bản là chi phí cho mỗi lớp. Mỗi lớp có các hàm ảo tạo ra một vtable
bộ nhớ lưu địa chỉ cho các hàm mà nó thực sự sẽ gọi (gửi ảo / động) khi thực hiện một cuộc gọi hàm ảo. Các vptr
cá thể được lưu trữ sau đó trỏ đến lớp cụ thể này vtable
. Chi phí này thường là một mối quan tâm ít hơn, nhưng nó có thể làm tăng kích thước nhị phân của bạn và thêm một chút chi phí thời gian chạy nếu chi phí này được trả một cách không cần thiết cho một nghìn lớp trong một cơ sở mã phức tạp, ví dụ: vtable
Chi phí này thực sự tăng tỷ lệ thuận với nhiều hơn và nhiều chức năng ảo trong hỗn hợp.
Các nhà phát triển Java làm việc trong các lĩnh vực quan trọng về hiệu năng hiểu rất rõ loại chi phí này (mặc dù thường được mô tả trong ngữ cảnh quyền anh), do loại do người dùng Java định nghĩa thừa hưởng từ một object
lớp cơ sở trung tâm và tất cả các chức năng trong Java đều là ảo (có thể ghi đè ) trong tự nhiên trừ khi được đánh dấu khác. Kết quả là, một Java Integer
tương tự có xu hướng yêu cầu 16 byte bộ nhớ trên các nền tảng 64 bit do kết quả của vptr
siêu dữ liệu kiểu này được liên kết cho mỗi cá thể và trong Java thường không thể bọc một cái gì đó giống như int
một lớp mà không phải trả thời gian chạy chi phí hiệu suất cho nó.
Sau đó, câu hỏi là: Tại sao c ++ không đặt tất cả các hàm hủy theo mặc định?
C ++ thực sự ưa thích hiệu năng với kiểu suy nghĩ "trả tiền khi bạn đi" và vẫn còn rất nhiều thiết kế điều khiển bằng phần cứng bằng kim loại được thừa hưởng từ C. Nó không muốn bao gồm một cách không cần thiết cho việc tạo vtable và công văn động cho mỗi lớp / trường hợp liên quan. Nếu hiệu suất không phải là một trong những lý do chính khiến bạn sử dụng ngôn ngữ như C ++, thì bạn có thể hưởng lợi nhiều hơn từ các ngôn ngữ lập trình khác ngoài kia vì rất nhiều ngôn ngữ C ++ kém an toàn và khó hơn so với lý tưởng thường có hiệu suất lý do chính để ủng hộ một thiết kế như vậy.
Khi nào tôi KHÔNG cần sử dụng các hàm hủy ảo?
Khá thường xuyên. Nếu một lớp không được thiết kế để kế thừa, thì nó không cần một hàm hủy ảo và cuối cùng sẽ chỉ phải trả một khoản chi phí lớn có thể cho thứ mà nó không cần. Tương tự như vậy, ngay cả khi một lớp được thiết kế để kế thừa nhưng bạn không bao giờ xóa các thể hiện của kiểu con thông qua một con trỏ cơ sở, thì nó cũng không yêu cầu một hàm hủy ảo. Trong trường hợp đó, một thực tiễn an toàn là xác định một hàm hủy không ảo được bảo vệ, như vậy:
class BaseClass
{
protected:
// Disallow deleting/destroying subclass objects through `BaseClass*`.
~BaseClass() {}
};
Trong trường hợp nào tôi KHÔNG nên sử dụng các hàm hủy ảo?
Nó thực sự dễ dàng hơn để bao phủ khi bạn nên sử dụng các hàm hủy ảo. Thông thường, nhiều lớp hơn trong cơ sở mã của bạn sẽ không được thiết kế để kế thừa.
std::vector
, chẳng hạn, không được thiết kế để kế thừa và thường không nên kế thừa (thiết kế rất run), vì điều đó sẽ dễ xảy ra vấn đề xóa con trỏ cơ sở này ( std::vector
cố tình tránh một hàm hủy ảo) ngoài các vấn đề cắt đối tượng vụng về nếu lớp dẫn xuất thêm bất kỳ trạng thái mới.
Nói chung, một lớp được kế thừa nên có một hàm hủy ảo ảo công khai hoặc một lớp được bảo vệ, không ảo. Từ C++ Coding Standards
, chương 50:
50. Làm cho các hàm hủy lớp cơ sở thành công khai và ảo, hoặc được bảo vệ và không ảo. Để xóa, hoặc không xóa; đó là câu hỏi: Nếu việc xóa thông qua một con trỏ đến Cơ sở cơ sở nên được cho phép, thì hàm hủy của Base phải là công khai và ảo. Nếu không, nó nên được bảo vệ và không ảo.
Một trong những điều mà C ++ có xu hướng nhấn mạnh ngầm (bởi vì các thiết kế có xu hướng trở nên thực sự giòn và khó xử và thậm chí có thể không an toàn nếu không) là ý tưởng rằng kế thừa không phải là một cơ chế được thiết kế để sử dụng như một suy nghĩ sau. Đó là một cơ chế mở rộng với tính đa hình trong tâm trí, nhưng một cơ chế đòi hỏi tầm nhìn xa về nơi cần mở rộng. Kết quả là, các lớp cơ sở của bạn nên được thiết kế như là gốc của hệ thống phân cấp thừa kế, và không phải là thứ bạn kế thừa từ sau này như một suy nghĩ trước mà không có bất kỳ tầm nhìn xa nào như vậy.
Trong những trường hợp đơn giản là bạn muốn kế thừa để sử dụng lại mã hiện có, thành phần thường được khuyến khích mạnh mẽ (Nguyên tắc tái sử dụng tổng hợp).