Câu hỏi GNU GCC (g ++): Tại sao nó tạo ra nhiều dtors?


Môi trường phát triển: GNU GCC (g ++) 4.1.2

Trong khi tôi đang cố gắng để điều tra làm thế nào để tăng 'mã vùng phủ sóng - đặc biệt là bảo hiểm chức năng' trong thử nghiệm đơn vị, tôi đã tìm thấy rằng một số dtor lớp dường như được tạo ra nhiều lần. Có một số bạn có bất kỳ ý tưởng về lý do tại sao, xin vui lòng?

Tôi đã thử và quan sát những gì tôi đã đề cập ở trên bằng cách sử dụng đoạn mã sau.

Trong "test.h"

class BaseClass
{
public:
    ~BaseClass();
    void someMethod();
};

class DerivedClass : public BaseClass
{
public:
    virtual ~DerivedClass();
    virtual void someMethod();
};

Trong "test.cpp"

#include <iostream>
#include "test.h"

BaseClass::~BaseClass()
{
    std::cout << "BaseClass dtor invoked" << std::endl;
}

void BaseClass::someMethod()
{
    std::cout << "Base class method" << std::endl;
}

DerivedClass::~DerivedClass()
{
    std::cout << "DerivedClass dtor invoked" << std::endl;
}

void DerivedClass::someMethod()
{
    std::cout << "Derived class method" << std::endl;
}

int main()
{
    BaseClass* b_ptr = new BaseClass;
    b_ptr->someMethod();
    delete b_ptr;
}

Khi tôi xây dựng đoạn mã trên (thử nghiệm g ++ test.cpp -o) và sau đó xem loại biểu tượng nào đã được tạo như sau,

thử nghiệm nm - demangle

Tôi có thể thấy kết quả sau.

==== following is partial output ====
08048816 T DerivedClass::someMethod()
08048922 T DerivedClass::~DerivedClass()
080489aa T DerivedClass::~DerivedClass()
08048a32 T DerivedClass::~DerivedClass()
08048842 T BaseClass::someMethod()
0804886e T BaseClass::~BaseClass()
080488f6 T BaseClass::~BaseClass()

Câu hỏi của tôi là như sau.

1) Tại sao nhiều dtors đã được tạo ra (BaseClass - 2, DerivedClass - 3)?

2) sự khác biệt giữa các dtors là gì? Làm thế nào những dtors nhiều sẽ được chọn lọc sử dụng?

Bây giờ tôi có cảm giác rằng để đạt được 100% phạm vi chức năng cho dự án C ++, chúng ta sẽ cần phải hiểu điều này để tôi có thể gọi tất cả các dtors đó trong các bài kiểm tra đơn vị của tôi.

Tôi sẽ đánh giá cao nếu ai đó có thể cho tôi câu trả lời ở trên.


76
2017-07-07 16:24


gốc


1 để bao gồm một chương trình mẫu tối thiểu, hoàn chỉnh. (sscce.org) - Robᵩ
Lớp cơ sở của bạn có cố ý có một destructor không ảo không? - Kerrek SB
Một quan sát nhỏ; bạn đã phạm tội, và không làm cho destructor BaseClass của bạn ảo. - Lyke
Xin lỗi vì mẫu chưa hoàn chỉnh của tôi. Có, BaseClass nên có destructor ảo để các đối tượng lớp có thể được sử dụng đa hình. - Smg
@ Lyke: tốt, nếu bạn biết rằng bạn sẽ không xóa một nguồn gốc thông qua một con trỏ đến cơ sở đó là OK, tôi đã chỉ làm cho chắc chắn ... vui vẻ, nếu bạn làm cho các thành viên cơ sở ảo, bạn nhận được nhiều hơn destructors. - Kerrek SB


Các câu trả lời:


Đầu tiên, mục đích của các chức năng này được mô tả trong Itanium C ++ ABI; xem các định nghĩa trong "destructor đối tượng cơ sở", "destructor đối tượng hoàn chỉnh" và "xóa destructor". Ánh xạ tới các tên bị xáo trộn được đưa ra trong 5.1.4.

Về cơ bản:

  • D2 là "destructor đối tượng cơ sở". Nó phá hủy chính đối tượng, cũng như các thành viên dữ liệu và các lớp cơ sở không ảo.
  • D1 là "destructor đối tượng hoàn chỉnh". Nó cũng phá hủy các lớp cơ sở ảo.
  • D0 là "xóa đối tượng destructor". Nó làm mọi thứ phá hủy đối tượng hoàn chỉnh, cộng với nó gọi operator delete để thực sự giải phóng bộ nhớ.

Nếu bạn không có các lớp cơ sở ảo, D2 và D1 giống nhau; GCC sẽ, trên mức tối ưu hóa đầy đủ, thực sự bí danh các biểu tượng cho cùng một mã cho cả hai.


58
2017-07-07 17:06



Cảm ơn bạn cho câu trả lời rõ ràng. Bây giờ tôi có thể liên quan đến, mặc dù tôi cần phải nghiên cứu nhiều hơn vì tôi không quá quen thuộc với loại thừa kế ảo của các chất liệu. - Smg
@Smg: trong thừa kế ảo, các lớp "kế thừa" được thừa hưởng dưới sự chịu trách nhiệm duy nhất của đối tượng có nguồn gốc cao nhất. Đó là, nếu bạn có struct B: virtual A và sau đó struct C: B, sau đó khi phá hủy B bạn gọi B::D1 mà lần lượt gọi A::D2 và khi phá hủy C bạn gọi C::D1 gọi B::D2 và A::D2 (lưu ý cách B::D2 không gọi A destructor). Điều thực sự tuyệt vời trong phân khu này là thực sự có thể quản lý tất cả các tình huống với một hệ thống phân cấp tuyến tính đơn giản của 3 destructors. - Matthieu M.
Hmm, tôi có thể không hiểu rõ điểm đó ... Tôi nghĩ rằng trong trường hợp đầu tiên (đối tượng B phá hủy), A :: D1 sẽ được gọi thay vì A :: D2. Và cũng trong trường hợp thứ hai (hủy đối tượng C), A :: D1 sẽ được gọi thay vì A :: D2. Liệu tôi có sai? - Smg
A :: D1 không được gọi vì A không phải là lớp cấp cao nhất ở đây; trách nhiệm phá hủy các lớp cơ sở ảo của A (có thể hoặc có thể không tồn tại) không thuộc về A, mà là cho D1 hoặc D0 của lớp cấp cao nhất. - bdonlan


Thường có hai biến thể của hàm tạo (không chịu trách nhiệm / phụ trách) và ba của destructor (không chịu trách nhiệm / phụ trách / xóa phí).

Các không chịu trách nhiệm ctor và dtor được sử dụng khi xử lý một đối tượng của một lớp thừa hưởng từ một lớp khác bằng cách sử dụng virtual từ khóa, khi đối tượng không phải là đối tượng hoàn chỉnh (vì vậy đối tượng hiện tại là "không chịu trách nhiệm" về việc xây dựng hoặc hủy đối tượng cơ sở ảo). Ctor này nhận một con trỏ tới đối tượng cơ sở ảo và lưu trữ nó.

Các phụ trách ctor và dtors là cho tất cả các trường hợp khác, tức là nếu không có thừa kế ảo nào liên quan; nếu lớp có một destructor ảo, xóa phí Con trỏ dtor đi vào khe vtable, trong khi phạm vi biết loại động của đối tượng (tức là đối với các đối tượng có thời gian lưu trữ tự động hoặc tĩnh) sẽ sử dụng phụ trách dtor (bởi vì bộ nhớ này không nên được giải phóng).

Ví dụ về mã:

struct foo {
    foo(int);
    virtual ~foo(void);
    int bar;
};

struct baz : virtual foo {
    baz(void);
    virtual ~baz(void);
};

struct quux : baz {
    quux(void);
    virtual ~quux(void);
};

foo::foo(int i) { bar = i; }
foo::~foo(void) { return; }

baz::baz(void) : foo(1) { return; }
baz::~baz(void) { return; }

quux::quux(void) : foo(2), baz() { return; }
quux::~quux(void) { return; }

baz b1;
std::auto_ptr<foo> b2(new baz);
quux q1;
std::auto_ptr<foo> q2(new quux);

Các kết quả:

  • Mục nhập dtor trong mỗi vtables cho foo, baz và quux điểm tại tương ứng xóa phí dtor.
  • b1 và b2 được xây dựng bởi baz()  phụ trách, cuộc gọi nào foo(1)  phụ trách
  • q1 và q2 được xây dựng bởi quux()  phụ trách, rơi foo(2)  phụ trách và baz()  không chịu trách nhiệm với một con trỏ đến foo đối tượng được xây dựng trước đó
  • q2 bị hủy bởi ~auto_ptr()  phụ trách, gọi dtor ảo ~quux()  xóa phí, cuộc gọi nào ~baz()  không chịu trách nhiệm, ~foo()  phụ trách và operator delete.
  • q1 bị hủy bởi ~quux()  phụ trách, cuộc gọi nào ~baz()  không chịu trách nhiệm và ~foo()  phụ trách
  • b2 bị hủy bởi ~auto_ptr()  phụ trách, gọi dtor ảo ~baz()  xóa phí, cuộc gọi nào ~foo()  phụ trách và operator delete
  • b1 bị hủy bởi ~baz()  phụ trách, cuộc gọi nào ~foo()  phụ trách

Bất cứ ai bắt nguồn từ quux sẽ sử dụng không chịu trách nhiệm ctor và dtor và chịu trách nhiệm tạo ra foo vật.

Về nguyên tắc, không chịu trách nhiệm biến thể không bao giờ cần thiết cho một lớp không có cơ sở ảo; trong trường hợp đó, phụ trách biến thể đôi khi được gọi là thống nhât, và / hoặc các biểu tượng cho cả hai phụ trách và không chịu trách nhiệm được đặt bí danh cho một triển khai đơn lẻ.


33
2017-07-07 17:48



Cảm ơn bạn đã giải thích rõ ràng của bạn kết hợp với ví dụ khá dễ hiểu. Trong trường hợp thừa kế ảo có liên quan, đó là trách nhiệm của lớp có nguồn gốc cao nhất để tạo đối tượng lớp cơ sở ảo. Đối với các lớp khác so với lớp dẫn xuất nhất, chúng được cho là được hiểu bởi hàm tạo không phụ trách để chúng không chạm vào lớp cơ sở ảo. - Smg