Reputation: 1254
I'm confused about the following C++ code (run it online at http://cpp.sh/8bmp). It combines several concepts I'm learning about in a course.
#include <iostream>
using namespace std;
class A {
public:
A() {cout << "A ctor" << endl;}
virtual ~A() {cout << "A dtor" << endl;}
};
class B: public A {
public:
B() {cout << "B ctor" << endl;}
~B() {cout << "B dtor" << endl;}
void foo(){cout << "foo" << endl;}
};
int main(){
B *b = new B[1];
b->~B();
b->foo();
delete b;
return 0;
}
Output:
A ctor
B ctor
B dtor
A dtor
foo
A dtor
Here's what I don't understand:
foo
after calling the destructor?delete
after calling the destructor?delete b;
will this code leak memory?A
is virtual. I thought virtual functions overloaded in subclasses wouldn't get called. Why does ~A()
get called then?b->~B();
then the line B dtor
is printed after foo
. Why?b->~B();
twice, then the output is: B dtor\nA dtor\nA dtor
. Huh?delete B;
with delete[] b;
. I think the second one is correct because b
is created with new[]
, but it doesn't matter because I'm only pushing one instance of B
to the heap. Is that correct?I'm sorry for asking so many questions, but this is pretty confusing to me. If my individual questions are misguided, then tell me what I need to know to understand when each destructor will run.
Upvotes: 1
Views: 118
Reputation: 721
Q1: Calling a method on a destroyed object is "Undefined Behaviour". Meaning that the standard does not specify what should happen. The idea behind UB is that they are supposed to be bugs in the application logic, but for performance reason, we do not force the compiler to do something special about it, as that would degrade performance for when we do things correctly.
In this case, because the foo() method does not depends on anything in the memory pointed by b
, it will work as expected. Just because the compiler does not do any test on it.
Q2: This is also "Undefined Behaviour". And you can see already some bizarre things are happening. First, the B destructor is not called, just the A destructor. What is happening is that when you called previously b->~B()
, the B destructor was called, then the vtable of the object was changed to a A vtable (meaning the object runtime type became A) and then the A destructor was called. When you called delete b
, the runtime called the virtual destructor of the object which was A. Like said previously, this is "Undefined Beheviour". The compiler chhose to generate code that works that way when calling delete b
, but it could have generated different code and still be right.
If fact, something worse has probably been done after calling the A destructor because of another bug in your code: you use delete
instead of delete[]
. C++ rules states that arrays allocated with operator new[]
must be freed using operator delete[]
and that using operator delete
is "Undefined behaviour". In fact, in most implementation I know, doing that will work the first time, but has a good chance of corrupting the memory management data, so that a future call to new
or delete
, even valid, may crash or cause memory leak.
Q3: There will be a memory leak if you remove the call. You will probably have memory corruption if you keep the call. Memory leak will be avoided if you use delete[] b
instead. There is still undefined behaviour because a destrutor will be called on the already destroyed object, but because these destructors do nothing, they will not likely do any more damage to your program
Q4: This is the rule with all destructors, not just virtual destructors: they will destroy the member objects then the base objects at the end of the code.
Q5: So when the compiler generates the code for the B destructor, it adds at the end a call to the A destructor. But there is a rule: at this time, this
is not a B object anymore and during the call to any virtual method in the A destructor, A methods must be called, not B methods, even if virtual. So before calling the A destructor, the B destructor will "downgrade" the dynamic type of the object from B to A (in practice, that means setting the vtable of the object to the A vtable). As the goal of the compiler is to generate efficient code, it does not have to change the vtable at the end of the A destructor. Remember: any method call on the object after the destructor is called is "Undefined Behaviour". Priority is performance, not bug detection.
Q6: same answer as Q5, and I also talked about is in Q2
Q7: It matters. A lot. delete[] needs to know the number of objects created so it can call the destrutors for all the objects in the array. Often implementations of new[] will actually allocate a size_t
element to store the number of objects in the array before the elements of the array. So the returned pointer is not the start of the allocated bloc, but the location after the size (4 bytes on 32 bits system, 8 bytes on 64 bits system). So first new B[1]
will allocate 4 or 8 bytes more that new B
and second delete[] will need to decrement the pointer by 4 or 8 bytes before deallocating it. So delete b
and delete[] b
are VERY different.
Note: compilers are not mandated to implement new[]
and delete[]
that way. And some implementations offer version of the runtime library that do more checkings so that bugs are easier to detect. However, for the best performance, delete[]
MUST be called if new[]
is used. And if you make the mistake to call delete
on a pointer allocated by new[]
, most of the time, it will not crash or fail the first time you do it. It will likely crash or fail later in a perfectly legal new
, new[]
, delete
or delete[]
operation. Which often means a lot of head scratching wondering why a perfectly correct operation is failing. This is the true meaning of "Undefined Behaviour"
Upvotes: 0
Reputation: 38238
- Why can I call
foo
after calling the destructor?
C++ doesn't stop you from shooting yourself in the foot. Just because you can do it (and the code doesn't immediately crash) doesn't mean it's legal or well defined.
- Why can I call
delete
after calling the destructor?
Same as answer #1.
- If I comment out
delete b;
will this code leak memory?
Yes. You must delete
what you new
(and delete[]
what you new[]
).
- The destructor for
A
is virtual. I thought virtual functions overloaded in subclasses wouldn't get called. Why does ~A() get called then?
I think the word you want is override, not overload. Anyway, you're not overriding ~A()
. Notice that ~B()
and ~A()
have different names.
Destructors are kinda special. When the derived class's destructor is finished running, it implicitly calls the base class's destructor. Why? Because the C++ standard says that's what will happen.
A virtual destructor is a special destructor. I lets you polymorphically delete an object. That means you can do code like the following:
B *b = new B;
A *a = b;
delete a; // Legal with virtual destructors, illegal without virtual.
If A
did not have a virtual destructor in the code above, it would not call ~B()
, which would be undefined behavior. With a virtual destructor, the compiler will correctly call ~B()
when delete a;
is run, even though a
is an A*
and not a B*
.
- If I comment out
b->~B();
then the lineB dtor
is printed afterfoo
. Why?
Because it's run after foo()
. delete b;
implicitly calls b
's destructor, which is after foo()
has already run.
- If I repeat the line
b->~B();
twice, then the output is:B dtor\nA dtor\nA dtor
. Huh?
It's undefined behavior. So anything can happen, really. Yeah, that's weird output. Undefined behavior is weird.
- I get the same output if I switch
delete B;
withdelete[] b;
. I think the second one is correct becauseb
is created withnew[]
, but it doesn't matter because I'm only pushing one instance ofB
to the heap. Is that correct?
It matters what you call. delete
and delete[]
are not the same thing. You can't call one in place of the other. You must call delete
only on memory that's been allocated with new
, and delete[]
with memory that's been allocated with new[]
. You cannot mix and match as you want. Doing so is undefined behavior.
You should be using delete[]
here, in this code, because you used new[]
.
Upvotes: 1
Reputation: 129494
"Undefined behaviour" (UB for short) is where the compiler is allowed to do anything - this commonly means somewhere between "crash", "give incorrect result" and "do what you'd expect anyway". Your b->foo()
is definitely undefined, since it happens after your b->~B()
call,
Since your foo
function doesn't actually USE anything that gets destroyed by the destructor, the call to foo
"works", because there is nothing being used that has been destroyed. [This is by no means guaranteed - it JUST HAPPENS to work, a bit like sometimes it's fine to cross a road without looking, at other times it's not. Depending on what road it is, it may be a really bad idea, or might work most of the time - but there is a reason people say "look left, look right, look left, then cross if it's safe" (or something like that)]
Calling delete
on an object that has been destroyed is also UB, so again, it's pure luck that it "works" (in the sense of "doesn't cause your program to crash").
Also mixing delete
with new []
or vice versa is UB - again, the compiler [and it's related runtime] may do the right or the wrong thing, depending on circumstances and conditions.
Do Not rely on undefined behaviour in your program [1]. It is bound to come back and bite you. C and C++ have quite a few UB-cases, and it's good to understand at least the most common cases, such as "use after destruction", "use after free" and such, and be on the lookout for such cases - and avoid it at all costs!
Upvotes: 6