Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Destructor called more times than it should. Bug in MSVC or not?

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)

like image 745
vladon Avatar asked Mar 02 '21 10:03

vladon


People also ask

Is destructor called after return?

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.

Why is my destructor being called C++?

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.


Video Answer


3 Answers

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.

like image 130
Ayxan Haqverdili Avatar answered Oct 23 '22 22:10

Ayxan Haqverdili


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.

like image 38
Alex Guteniev Avatar answered Oct 23 '22 20:10

Alex Guteniev


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.

like image 3
xmh0511 Avatar answered Oct 23 '22 21:10

xmh0511