This program:
#include <iostream>
using namespace std;
struct B {
B() { cout << "B"; }
//B(const B& b) { cout << "copyB"; }
~B() { cout << "~B"; }
};
struct C : B {
};
void f(B b) {
}
int main() {
C c;
f(c);
return 0;
}
outputs B~B~B~B
, i.e. three times calling destructor, why?
Only in MSVC. Clang and GCC outputs B~B~B
(which is most likely correct).
And interesting thing: if you uncomment copy-ctor, it outputs BcopyB~B~B
, which is correct (destructor called two times).
Is it a bug in MSVC compiler? Or it is correct behavior?
(Visual Studio 2019 latest, cl.exe
version 19.28.29337)
However, in your current implementation you're actually returning a shallow copy of NodeContainer. Once your copy goes out of scope its destructor is called, which deallocates its memory, which in this case is the original memory of your member, effectively making your member invalid.
Destructors are called when one of the following events occurs: A local (automatic) object with block scope goes out of scope. An object allocated using the new operator is explicitly deallocated using delete . The lifetime of a temporary object ends.
If you print the addresses:
#include <stdio.h>
struct B {
B() { printf(" B() <%p>\n", (void*)this); }
~B() { printf("~B() <%p>\n", (void*)this); }
};
struct C : B { };
void f(B b) { }
int main() {
C c;
f(c);
}
The output is:
B() <000000A013FFFAC4>
~B() <000000A013FFFAA0>
~B() <000000A013FFFBA4>
~B() <000000A013FFFAC4>
As you can see, no object is destructed 2 times. Seems like there is a temporary involved, which is, as far as I know, not allowed. This leads me to believe that this is a bug. This does only happen when the copy constructor is trivial. Since the destructor is not trivial, it's observable behavior and not covered under the as-if rule.
Looking into Godbolt's Compiler Explorer compilation result with optimization disabled, see this main
function:
sub rsp, 56 ; 00000038H
lea rcx, QWORD PTR c$[rsp]
call C::C(void)
npad 1
movzx eax, BYTE PTR $T2[rsp]
mov BYTE PTR $T1[rsp], al
movzx ecx, BYTE PTR $T1[rsp]
call void f(B) ; f
npad 1
lea rcx, QWORD PTR $T2[rsp]
call B::~B(void) ; B::~B
npad 1
lea rcx, QWORD PTR c$[rsp]
call C::~C(void)
xor eax, eax
add rsp, 56 ; 00000038H
ret 0
So c$[rsp]
is variable c
on stack. It is sliced to temporary $T2[rsp]
of type B here, and then copied to temporary $T1[rsp]
, then $T1[rsp]
is passed to f(B)
and destroyed there, and $T2[rsp]
is destroyed here locally.
It is also noticeable that copying of B is dove via al
register, but slicing C to B is no-op. Looks like both are fine for class without members; we we add members, we'll see both slicing C to b and copying of B.
I'm not sure if Standard permits or prohibits making separate copy for conversion (slicing) and for passing by value, but this is what seem to happen here.
The conclusion is it's a bug of MSVC. Although the standard specifies a temporary object can be created when passing the argument to a function, however, many restrictions should be obeyed. The relevant rule is written in the following:
[class.temporary#3]
When an object of class type X is passed to or returned from a function, if X has at least one eligible copy or move constructor ([special]), each such constructor is trivial, and the destructor of X is either trivial or deleted, implementations are permitted to create a temporary object to hold the function parameter or result object. The temporary object is constructed from the function argument or return value, respectively, and the function's parameter or return object is initialized as if by using the eligible trivial constructor to copy the temporary (even if that constructor is inaccessible or would not be selected by overload resolution to perform a copy or move of the object).
In your example, the passed argument is of type class C
, it's derived from the base class B
which has a non-user-provided copy constructor and a user-provided destructor, which will result in the derived class C
can have a trivial copy constructor but cannot have a trivial destructor as per [class.copy.ctor#11.2] and [class.dtor#8.2], they're the following rules:
A copy/move constructor for class X is trivial if it is not user-provided and if:
- the constructor selected to copy/move each direct base class subobject is trivial
A destructor is trivial if it is not user-provided and if:
- all of the direct base classes of its class have trivial destructors
Not all of these restrictions will be satisfied by the class C
, hence the temporary object cannot be created in this case. That means the parameter should be initialized from the argument by using the copy constructor. Such two objects with automatic storage duration will be destroyed at the point the block exited, respectively. That is, only two times the ~B
should be printed here.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With