2009-06-22 70 views
65

Giả sử chúng ta có một (đồ chơi) lớp C++ như sau:Liệu một hàm tạo hoặc hàm hủy 'rỗng' có thực hiện tương tự như một hàm tạo ra không?

class Foo { 
    public: 
     Foo(); 
    private: 
     int t; 
}; 

Vì không có destructor được xác định, một trình biên dịch C++ nên tạo một cách tự động cho lớp Foo. Nếu destructor không cần phải dọn sạch bất kỳ bộ nhớ nào được cấp phát động (nghĩa là, chúng ta có thể dựa vào destructor một cách hợp lý mà trình biên dịch cung cấp cho chúng ta), sẽ định nghĩa một destructor rỗng, nghĩa là.

Foo::~Foo() { } 

làm điều tương tự như trình biên dịch tạo ra? Điều gì về một nhà xây dựng rỗng - đó là, Foo::Foo() { }?

Nếu có sự khác biệt, chúng tồn tại ở đâu? Nếu không, là một phương pháp ưa thích hơn khác?

+0

Tôi đã sửa đổi câu hỏi này một chút để biến chỉnh sửa sau này thành một phần thực tế của câu hỏi. Nếu có bất kỳ lỗi cú pháp nào trong các phần tôi đã chỉnh sửa, hãy hét lên với tôi, không phải người hỏi ban đầu. @Andrew, nếu bạn cảm thấy như tôi đã thay đổi câu hỏi của bạn quá nhiều, hãy hoàn nguyên nó; nếu bạn thích sự thay đổi nhưng nghĩ rằng nó không đủ, bạn rõ ràng được chào đón để chỉnh sửa câu hỏi của riêng bạn. –

Trả lời

112

Nó sẽ làm điều tương tự (không có gì, về bản chất). Nhưng nó không giống như khi bạn không viết nó. Bởi vì việc viết destructor sẽ yêu cầu một trình phá hủy lớp cơ sở làm việc. Nếu destructor lớp cơ sở là riêng tư hoặc nếu có bất kỳ lý do nào khác mà nó không thể được gọi, thì chương trình của bạn bị lỗi. Xem xét việc này

struct A { private: ~A(); }; 
struct B : A { }; 

Đó là OK, miễn là bạn không cần phải hủy một đối tượng kiểu B (và do đó, mặc nhiên loại A) - như thế nào nếu bạn không bao giờ gọi xóa trên một đối tượng tự động tạo ra, hoặc bạn không bao giờ tạo ra một đối tượng của nó ở nơi đầu tiên. Nếu bạn làm thế, trình biên dịch sẽ hiển thị một chẩn đoán thích hợp. Bây giờ nếu bạn cung cấp một cách rõ ràng

struct A { private: ~A(); }; 
struct B : A { ~B() { /* ... */ } }; 

Đó là một sẽ cố gắng ngầm gọi destructor của các cơ sở hạng nhất, và sẽ gây ra một chẩn đoán đã vào thời điểm định nghĩa của ~B.

Có một sự khác biệt khác tập trung vào định nghĩa về hàm hủy và các cuộc gọi ngầm đến các trình phá hủy thành viên.Xem xét thành viên con trỏ thông minh này

struct C; 
struct A { 
    auto_ptr<C> a; 
    A(); 
}; 

Giả sử đối tượng của loại C được tạo ra trong định nghĩa của constructor của một trong file .cpp, mà cũng chứa các định nghĩa của struct C. Bây giờ, nếu bạn sử dụng struct A, và yêu cầu tiêu hủy đối tượng A, trình biên dịch sẽ cung cấp một định nghĩa ngầm định về hàm hủy, giống như trong trường hợp trên. Đó là destructor cũng sẽ ngầm gọi là destructor của đối tượng auto_ptr. Và điều đó sẽ xóa con trỏ nó giữ, trỏ đến đối tượng C - mà không biết định nghĩa của C! Điều đó xuất hiện trong tệp .cpp trong đó hàm tạo của struct A được định nghĩa.

Đây thực sự là một vấn đề phổ biến trong việc triển khai thành ngữ pimpl. Giải pháp ở đây là thêm một destructor và cung cấp một định nghĩa rỗng của nó trong tệp .cpp, trong đó struct C được định nghĩa. Tại thời điểm nó gọi destructor của thành viên của nó, sau đó nó sẽ biết định nghĩa của struct C, và có thể gọi chính xác destructor của nó.

struct C; 
struct A { 
    auto_ptr<C> a; 
    A(); 
    ~A(); // defined as ~A() { } in .cpp file, too 
}; 

Lưu ý rằng boost::shared_ptr không có vấn đề đó: Thay vào đó, yêu cầu loại hoàn chỉnh khi hàm tạo của nó được gọi theo một số cách nhất định.

Một điểm khác mà nó tạo sự khác biệt trong C++ hiện tại là khi bạn muốn sử dụng memset và bạn bè trên đối tượng như vậy có người dùng đã khai báo destructor. Các loại như vậy không phải là POD nữa (dữ liệu cũ thuần túy), và chúng không được phép sao chép bit. Lưu ý rằng hạn chế này không thực sự cần thiết - và phiên bản C++ tiếp theo đã cải thiện tình huống này, do đó nó cho phép bạn vẫn sao chép bit các loại đó, miễn là các thay đổi quan trọng khác không được thực hiện.


Vì bạn đã yêu cầu nhà thầu: Vâng, vì những điều tương tự cũng đúng. Lưu ý rằng các hàm tạo cũng chứa các cuộc gọi ngầm đến các trình phá hủy. Về những thứ như auto_ptr, các cuộc gọi này (ngay cả khi không thực sự thực hiện khi chạy - khả năng thuần túy đã có vấn đề ở đây) sẽ làm hại tương tự như đối với các destructors, và xảy ra khi một cái gì đó trong constructor ném - trình biên dịch sau đó được yêu cầu gọi hàm hủy của các thành viên. This answer làm cho một số sử dụng định nghĩa ngầm định của các hàm tạo mặc định.

Ngoài ra, điều này cũng đúng với mức hiển thị và POD mà tôi đã nói về trình phá hủy ở trên.

Có một sự khác biệt quan trọng liên quan đến việc khởi tạo. Nếu bạn đặt một người sử dụng khai báo constructor, kiểu của bạn không nhận được giá trị khởi tạo của các thành viên nữa, và nó là vào constructor của bạn để làm bất kỳ khởi tạo đó là cần thiết. Ví dụ:

struct A { 
    int a; 
}; 

struct B { 
    int b; 
    B() { } 
}; 

Trong trường hợp này, sau đây là luôn luôn đúng

assert(A().a == 0); 

Trong khi đây là hành vi không xác định, bởi vì b không bao giờ được khởi tạo (constructor của bạn bỏ qua đó). Giá trị có thể bằng 0, nhưng có thể là bất kỳ giá trị lạ nào khác. Cố gắng đọc từ một đối tượng không được khởi tạo như vậy gây ra hành vi không xác định.

assert(B().b == 0); 

Điều này cũng đúng cho việc sử dụng cú pháp này trong new, như new A() (chú ý dấu ngoặc ở cuối - nếu họ bị bỏ qua giá trị khởi tạo không được thực hiện, và vì không có người sử dụng tuyên bố constructor có thể khởi tạo nó , a sẽ không được khởi tạo).

+0

+1 để đề cập đến các con trỏ tự động được khai báo chuyển tiếp và trình tự động hủy. Một dấu hiệu phổ biến khi bạn bắt đầu khai báo các công cụ. –

+1

Ví dụ đầu tiên của bạn hơi lạ. B bạn đã viết không thể được sử dụng ở tất cả (một cái mới sẽ là một lỗi, bất kỳ một diễn viên nào sẽ là một hành vi không xác định, vì nó không phải là POD). –

+0

Ngoài ra, A(). A == 0 chỉ đúng đối với các số liệu thống kê. Biến cục bộ loại A sẽ không được khởi tạo. –

8

Vâng, hàm hủy rỗng đó giống với hàm hủy tự động. Tôi đã luôn luôn chỉ để cho trình biên dịch tạo ra chúng tự động; Tôi không nghĩ rằng nó là cần thiết để xác định destructor một cách rõ ràng trừ khi bạn cần phải làm một cái gì đó bất thường: làm cho nó ảo hoặc tư nhân, nói.

11

Trình phá hủy rỗng mà bạn đã xác định trong lớp có ngữ nghĩa tương tự trong hầu hết các trường hợp, nhưng không phải trong tất cả.

Cụ thể, destructor ngầm được xác định
1) là một inline viên công cộng (bạn không phải là inline)
2) được ký hiệu là một destructor tầm thường (cần thiết để làm cho các loại tầm thường mà có thể là trong các đoàn thể, của bạn không thể)
3) có đặc điểm ngoại lệ (ném(), của bạn không)

+1

Lưu ý về 3: Đặc tả ngoại lệ không phải lúc nào cũng trống trong một destructor được xác định ngầm, như được ghi chú trong [except.spec]. – dalle

+0

@dalle +1 trên bình luận - cảm ơn vì đã chú ý đến điều đó - bạn thực sự đúng, nếu Foo đã bắt nguồn từ các lớp cơ sở với các trình phá hoại không ngầm với các đặc tả ngoại lệ - thì dtor ngầm của Foo sẽ "thừa hưởng" sự kết hợp của những ngoại lệ đó thông số kỹ thuật - trong trường hợp này, vì không có thừa kế, đặc tả ngoại lệ của dtor ngầm sẽ xảy ra là throw(). –

1

Tôi muốn nói rõ nhất là đặt bản khai trống, nó nói với bất kỳ người bảo trì nào trong tương lai rằng nó không phải là giám sát, và bạn thực sự đã có nghĩa là sử dụng một mặc định.

2

Tôi đồng ý với David ngoại trừ việc tôi sẽ nói đó là một thói quen tốt để xác định một destructor ảo tức là

virtual ~Foo() { } 

bỏ lỡ destructor ảo có thể dẫn đến rò rỉ bộ nhớ vì những người kế thừa từ lớp Foo của bạn có thể đã không nhận thấy rằng destructor của họ sẽ không bao giờ được gọi là !!

0

Một định nghĩa sản phẩm nào là tốt kể từ khi định nghĩa có thể được tham chiếu

virtual ~GameManager() { };
Việc kê khai rỗng là tưởng như tương tự xuất hiện
virtual ~GameManager();
chưa mời sợ hãi không có định nghĩa cho destructor ảo lỗi
Undefined symbols: 
    "vtable for GameManager", referenced from: 
     __ZTV11GameManager$non_lazy_ptr in GameManager.o 
     __ZTV11GameManager$non_lazy_ptr in Main.o 
ld: symbol(s) not found

16

Tôi biết tôi đến trễ trong thảo luận, tuy nhiên kinh nghiệm của tôi nói rằng trình biên dịch hoạt động khác nhau khi đối mặt với một destructor trống so với một trình biên dịch được tạo ra. Ít nhất đây là trường hợp với MSVC++ 8.0 (2005) và MSVC++ 9.0 (2008).

Khi xem cụm từ đã tạo cho một số mã sử dụng mẫu biểu thức, tôi nhận ra rằng ở chế độ phát hành, lệnh gọi đến số BinaryVectorExpression operator + (const Vector& lhs, const Vector& rhs) của tôi không bao giờ được gạch chân. (xin vui lòng không chú ý đến các loại chính xác và chữ ký của nhà điều hành).

Để chẩn đoán thêm sự cố, tôi đã bật Compiler Warnings That Are Off by Default khác nhau. Cảnh báo C4714 đặc biệt thú vị. Nó được phát ra bởi trình biên dịch khi một hàm được đánh dấu bằng __forceinlinekhông nhận được nội tuyến.

Tôi đã bật cảnh báo C4714 và tôi đã đánh dấu toán tử là __forceinline và tôi có thể xác minh báo cáo trình biên dịch không thể trực tiếp cuộc gọi đến toán tử.

Trong số những lý do được mô tả trong tài liệu, trình biên dịch không nội tuyến là một chức năng được đánh dấu bằng __forceinline cho:

Chức năng trả lại một đối tượng unwindable bởi giá trị khi -GX/EHS/EHA là trên

Đây là trường hợp của tôi BinaryVectorExpression operator + (const Vector& lhs, const Vector& rhs). BinaryVectorExpression được trả về theo giá trị và mặc dù hàm hủy của nó trống, nó làm cho giá trị trả lại này được coi là một đối tượng có thể bung ra. Thêm throw() vào trình phá hủy không giúp trình biên dịch và I avoid using exception specifications anyway. Nhận xét ra các destructor rỗng cho phép trình biên dịch nội tuyến hoàn toàn mã.

Việc lấy đi là từ bây giờ, trong mỗi lớp, tôi viết nhận xét về những kẻ hủy diệt trống để cho con người biết destructor không có mục đích gì cả, cũng giống như cách mọi người bình luận ra đặc tả ngoại lệ trống rỗng.) */để chỉ ra rằng destructor không thể ném.

//~Foo() /* throw() */ {} 

Hy vọng điều đó sẽ hữu ích.

Các vấn đề liên quan