Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Temporary lifetime variable MSVC different behaviour from GCC

Tags:

c++

I'm wondering if anyone can explain the difference in behaviour between GCC/Clang and MSVC for this code:

#include <iostream>

class Base
{
public:
    Base(int* a) : ptr{a} { std::cout << "Base construct\n"; }
    ~Base() { std::cout << "Base destruct\n"; }

    int* ptr;
};

class Derived : public Base
{
public:
    Derived() : Base(&val) { std::cout << "Derived construct\n"; }

    ~Derived() { std::cout << "Derived destruct\n"; }
protected:
    int val{2};
};

Derived GiveMeDerived() {
    return {};
}

void SomeFunc(Base b) {
    std::cout << "SomeFunc " << *b.ptr << "\n";
}

int main()
{
    SomeFunc(GiveMeDerived());

    return 0;
}

In GCC/Clang, I see the expected printout order of:

Base construct
Derived construct
SomeFunc 2
Base destruct
Derived destruct
Base destruct

but in MSVC 2022 (MSVC 19.39.33523.0), I get:

Base construct
Derived construct
SomeFunc 2
Base destruct
Base destruct
Derived destruct
Base destruct

Where does the extra base destruct come from?

When I change to code to try to track creation:

#include <iostream>

class Base
{
public:
    // Base() { std::cout << "Default\n"; }

    Base(int* a) : ptr{a} { std::cout << "Base construct\n"; }
    ~Base() { std::cout << "Base destruct " << default_des << "\n"; }

    int* ptr;
    std::string default_des{"default"};
};

class Derived : public Base
{
public:
    Derived() : Base(&val) { std::cout << "Derived construct\n"; }

    ~Derived() { std::cout << "Derived destruct\n"; }
protected:
    int val{2};
};

Derived GiveMeDerived() {
    Derived a;
    a.default_des = "From GiveMeDerived\n";
    return a;
}

void SomeFunc(Base b) {
    std::cout << "SomeFunc " << *b.ptr << "\n";
    b.default_des = "inside SomeFunc";
}

int main()
{
    SomeFunc(GiveMeDerived());

    return 0;
}

MSVC seems to "fix" itself and does not have the extra destructor:

Base construct
Derived construct
SomeFunc 2
Base destruct inside SomeFunc
Derived destruct
Base destruct From GiveMeDerived
like image 429
kekpirat Avatar asked Oct 24 '25 15:10

kekpirat


1 Answers

This is a bug in the Microsoft x86 calling convention.

If you cross compile with clang to Windows, you see the same behaviour in clang (which clang states is non-conforming but implements to act the same as MSVC).

[class.temporary]p3 in the standard allows compilers to introduce temporaries for function arguments. However, this is only for types that are trivially destructible.

The Microsoft ABI skips that check and passes Base in a register anyways, requiring a copy. It is as-if you wrote:

{
    Derived required_temporary = GiveMeDerived();
    Base invented_temporary = std::move(required_temporary);
    SomeFunc(std::move(invented_temporary));
}

Where required_temporary is the temporary that's materialized from the return value of GiveMeDerived(), but invented_temporary is non-conforming.

(If you run this on windows, it will create yet another non-conforming temporary for the argument of SomeFunc)


This is also the reason that this disappears when you add a std::string member: the class is no longer trivially copyable, so the calling convention doesn't call for it to be passed in a register. It also becomes too big.

like image 116
Artyer Avatar answered Oct 26 '25 04:10

Artyer