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
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.
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