Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Destructor of a function argument being called differently in gcc and MSVC

While porting some C++ code from Microsoft Visual Studio to gcc, I ran into a weird bug, which I eventually boiled down to this:

#include <iostream>
using namespace std;

class Foo {
public:
    int data;
    Foo(int i) : data(i) 
    {
        cout << "Foo constructed with " << i << endl; 
    }
    Foo(const Foo& f) : data(f.data)
    {
        cout << "copy ctor " << endl;
    }
    Foo(const Foo&& f) : data(f.data)
    {
        cout << "move ctor" << endl;
    }
    ~Foo()
    {
        cout << "Foo destructed with " << data << endl;
    }
};

int Bar(Foo f)
{
    cout << "f.data = " << f.data << endl;
    return f.data * 2;
}

int main()
{
    Foo f1(10);
    Foo f2(Bar(std::move(f1)));
}

If I compile and run the above code with Microsoft Visual Studio 2015 Community, I get the following output:

Foo constructed with 10
move ctor
f.data = 10
Foo destructed with 10
Foo constructed with 20
Foo destructed with 20
Foo destructed with 10

However, if I compile and run the code with gcc 6.1.1 and --std=c++14, I get this output:

Foo constructed with 10
move ctor
f.data = 10
Foo constructed with 20
Foo destructed with 10
Foo destructed with 20
Foo destructed with 10

gcc calls the destructor of f, the argument to Bar(), after Bar() returns, while msvc calls the destructor (apparently) before it returns, or at least before the constructor of f2. When is f supposed to be destructed, according to the C++ standard?

like image 748
Marek Pokorny Avatar asked May 11 '16 07:05

Marek Pokorny


1 Answers

They are all right; it depends. It seems underspecified in the standard.

From [expr.call]/4 (this wording goes back to C++98);

The lifetime of a parameter ends when the function in which it is defined returns. The initialization and destruction of each parameter occurs within the context of the calling function.

And the CWG#1880;

WG decided to make it unspecified whether parameter objects are destroyed immediately following the call or at the end of the full-expression to which the call belongs.

Both the behaviour of g++ (and clang) and MSVC would be correct, implementations are free to pick one approach over the other.

That all said, if the code you have is dependent on this ordering, I would change it such that the ordering is more deterministic - as you have seen, it leads to subtle bugs.


A simplified example of this behaviour is;

#include <iostream>
struct Arg {
    Arg() {std::cout << 'c';}
    ~Arg() {std::cout << 'd';}
    Arg(Arg const&)  {std::cout << 'a';}
    Arg(Arg&&)  {std::cout << 'b';}
    Arg& operator=(Arg const&)  {std::cout << 'e'; return *this;}
    Arg& operator=(Arg&&)  {std::cout << 'f'; return *this;}
};
void func(Arg) {}
int main() {
    (func(Arg{}), std::cout << 'X');
    std::cout << std::endl;
}

Clang and g++ both produce cXd and MSVC produces cdX.

like image 108
Niall Avatar answered Oct 04 '22 11:10

Niall