12

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?

  • `f`, being a named argument of `Bar()`, is not a temporary. In fact, I don't see any temporary. The other two `Foo`s are named f1 and f2. Not sure if there's an actual question here. – MSalters May 11 '16 at 07:15
  • I 'm probably confused about what a temporary is. My question is when is the argument `f` of `Bar()` supposed to be destructed, and which implementation, msvc or gcc, is correct? – Marek Pokorny May 11 '16 at 07:20
  • @MSalters I don't see how this is a duplicate of that question. That is about the order of construction/destruction in `f(a, b, c)` but this question never calls a function with more than 1 argument – M.M May 11 '16 at 07:24
  • 1
    @M.M: Check the answer as well. This question is a simpler version of the more general problem in the linked question. – MSalters May 11 '16 at 07:28
  • This comment on the duplicate is relevant; http://stackoverflow.com/questions/36992569/what-is-the-order-of-destruction-of-function-parameters#comment61627914_36992615 – Niall May 11 '16 at 07:33
  • 1
    OK, so the answer to this question is that in C++14 the `f` in `Bar(Foo f)` must be destroyed before `f2` is constructed (so g++ is wrong), however CWG decided to change this (perhaps in response to compilers getting it wrong). – M.M May 11 '16 at 07:44

1 Answers1

10

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.

Niall
  • 30,036
  • 10
  • 99
  • 142