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


89

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

Trong khi tôi đang cố gắng điều tra cách tăng 'mức độ bao phủ mã - đặc biệt là mức độ bao phủ chức năng' trong kiểm thử đơn vị, tôi nhận thấy rằng một số lớp dtor dường như được tạo nhiều lần. Có ai trong số các bạn có ý kiến ​​về lý do tại sao không?

Tôi đã thử và quan sát những gì tôi đã đề cập ở trên bằng cách sử dụng 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 tạo mã ở trên (g ++ test.cpp -o test) và sau đó xem loại ký hiệu nào đã được tạo như sau,

nm - kiểm tra hình tam giác

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 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 này là gì? Làm thế nào nhiều dtors đó sẽ được sử dụng có chọn lọc?

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

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


5
+1 để bao gồm một chương trình mẫu hoàn chỉnh, tối thiểu. ( sscce.org )
Robᵩ

2
Lớp cơ sở của bạn có cố ý có một trình hủy không ảo không?
Kerrek SB

2
Một quan sát nhỏ; bạn đã phạm tội và không làm cho trình hủy BaseClass của bạn ảo.
Lyke

Xin lỗi vì mẫu không đầy đủ của tôi. Có, BaseClass nên có hàm hủy ảo để các đối tượng lớp này có thể được sử dụng đa hình.
Smg

1
@Lyke: tốt, nếu bạn biết rằng bạn sẽ không xóa một dẫn xuất thông qua một con trỏ đến cơ sở, điều đó không sao, tôi chỉ đảm bảo rằng ... vui là nếu bạn làm cho các thành viên cơ sở ảo, bạn sẽ thậm chí nhiều trình hủy hơn .
Kerrek SB

Câu trả lời:


73

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

Về cơ bản:

  • D2 là "bộ hủy đối tượng cơ sở". Nó tự hủy đố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à "bộ hủy đối tượng hoàn chỉnh". Nó cũng phá hủy các lớp cơ sở ảo.
  • D0 là "trình hủy đối tượng xóa". Nó thực hiện mọi thứ mà trình hủy đối tượng hoàn chỉnh làm, 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ó lớp cơ sở ảo, D2 và D1 giống hệt nhau; GCC, trên các mức tối ưu hóa đủ, sẽ thực sự đặt biệt danh các ký hiệu cho cùng một mã cho cả hai.


Cảm ơn bạn đã trả lời rõ ràng. Bây giờ tôi có thể liên tưởng đến, mặc dù tôi cần phải nghiên cứu thêm vì tôi không quá quen thuộc với các loại thừa kế ảo.
Smg

@Smg: trong kế thừa ảo, các lớp kế thừa "hầu như" nằm dưới khả năng đáp ứng duy nhất của đối tượng có nguồn gốc nhất. Có nghĩa là, nếu bạn có struct B: virtual Avà sau đó struct C: B, thì khi phá hủy một, Bbạn sẽ gọi B::D1thứ mà lần lượt gọi ra A::D2và khi phá hủy một, Cbạn gọi ra lệnh C::D1nào B::D2A::D2(lưu ý cách B::D2không gọi A hủy). Đ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 gồm 3 hàm hủy.
Matthieu M.

Hmm, tôi có thể đã không hiểu điểm rõ ràng ... Tôi nghĩ rằng trong trường hợp đầu tiên (phá hủy đối tượng B), 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ể tồn tại hoặc có thể không) không thuộc về A, mà thuộc về D1 hoặc D0 của lớp cấp cao nhất.
bdonlan

37

Thường có hai biến thể của các nhà xây dựng ( không-phụ trách / phụ trách ) và ba trong số các destructor ( không-phụ trách / phụ trách / phụ trách xóa ).

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

Các chuyên trách ctor và dtors là dành cho tất cả các trường hợp khác, tức là nếu không có thừa kế ảo liên quan; nếu lớp có bộ hủy ảo, con trỏ dtor xóa phụ trách sẽ đi vào khe vtable, trong khi phạm vi biết kiểu động của đối tượng (tức là đối với các đối tượng có thời lượng lưu trữ tự động hoặc tĩnh) sẽ sử dụng dtor tích hợp (vì vùng nhớ này không đượ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 dtor trong mỗi vtables cho foo, bazquuxđiểm tại tương ứng phụ trách xóa dtor.
  • b1b2được xây dựng bởi baz() phụ trách , gọi foo(1) phụ trách
  • q1q2được xây dựng bởi quux() phụ trách , foo(2) phụ tráchbaz() không phụ trách bằng một con trỏ đến foođối tượng mà nó đã xây dựng trước đó
  • q2bị hủy bởi ~auto_ptr() in-charge , gọi là ~quux() xóa dtor in-charge ảo , gọi là ~baz() not-in-charge , ~foo() in-chargeoperator delete.
  • q1bị hủy bởi ~quux() phụ trách , gọi là ~baz() không phụ trách~foo() phụ trách
  • b2bị hủy bởi ~auto_ptr() phụ trách , gọi là ~baz() xóa dtor phụ trách ảo , gọi ~foo() phụ tráchoperator delete
  • b1bị hủy bởi ~baz() phụ trách , mà gọi ~foo() phụ trách

Bất kỳ ai đến từ quuxsẽ sử dụng ctor và dtor không phụ trách của nó và chịu trách nhiệm tạo foođối tượng.

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


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

Cảm ơn vì đã giải thích rõ ràng. Tôi muốn làm rõ thêm điều gì, nếu chúng ta không sử dụng auto_ptr và thay vào đó cấp phát bộ nhớ trong hàm tạo và xóa trong hàm hủy. Trong trường hợp đó, chúng ta sẽ chỉ xóa hai trình hủy không phụ trách / phụ trách xóa?
nonenone

1
@bhavin, không, thiết lập vẫn hoàn toàn giống nhau. Mã được tạo cho bộ hủy luôn hủy chính đối tượng và bất kỳ đối tượng con nào, vì vậy bạn nhận được mã cho deletebiểu thức như một phần của bộ hủy của riêng bạn hoặc như một phần của các lệnh gọi bộ hủy đối tượng con. Các deletebiểu hiện được thực hiện hoặc như một cuộc gọi thông qua vtable của đối tượng nếu nó có một destructor ảo (nơi chúng ta tìm phụ trách xóa , hoặc như là một cuộc gọi trực tiếp tới đối tượng là phụ trách destructor.
Simon Richter

A delete biểu thức không bao giờ gọi biến thể không phụ trách , biến thể đó chỉ được sử dụng bởi các trình hủy khác trong khi hủy một đối tượng sử dụng kế thừa ảo.
Simon Richter
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.