Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Difference observed between MSVC14.1 and gcc8.3 when a destructor call for an automatic variable amends a function return value

Tags:

c++

c++14

Consider the following (to be compiled with C++14)

#include <iostream>
#include <vector>

// Foo adds an element to a std::vector passed by reference
// on construction in the destructor
struct Foo {
    Foo(std::vector<double>& v) : m_v(v){
    }
    ~Foo(){
        m_v.push_back(1.0);
    }
    std::vector<double>& m_v;
};

std::vector<double> bar(){
    std::vector<double> ret;
    Foo foo(ret);
    return ret;
}

int main(){
    std::cout << bar().size() << "\n";
}

In gcc8.3 the output is 1, which means that foos destructor has an effect on the returned vector.

In MSVC14.1 the output is 0. You can force the output to be the same as gcc8.3 by replacing the line Foo foo(ret); with {Foo foo(ret);} (i.e. by forcing the scope).

I don't think this is dangling reference undefined behaviour (because ret is declared before foo) but rather that this could be a bug in MSVC14.1 (and I will create a bug report if so). Does anyone know for sure?

See https://ideone.com/pnxWuJ

like image 301
Bathsheba Avatar asked Nov 23 '20 22:11

Bathsheba


2 Answers

I think the relevant part of the C++20 standard is [stmt.return], which says

The copy-initialization of the result of the call is sequenced before the destruction of temporaries at the end of the full-expression established by the operand of the return statement, which, in turn, is sequenced before the destruction of local variables (8.7) of the block enclosing the return statement

So the result of the function call (the return value) should be constructed first, then foo is destroyed. Since the returned value is constructed before foo's destructor is run, the result should be 0.

The [class.copy.elision] section says

When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the constructor selected for the copy/move operation and/or the destructor for the object have side effects.

So both compilers can be considered correct.

like image 186
1201ProgramAlarm Avatar answered Nov 20 '22 20:11

1201ProgramAlarm


I think both compiler are right. NRVO is a common and permitted optimization. If that optimization indeed happen, you're gonna see the item inserted in it by the destructor.

If NRVO don't happen, then just like @1201ProgramAlarm said, the expected behavior is to return the empty vector.

To quote the same content as his answer,

The copy-initialization of the result of the call is sequenced before the destruction of temporaries at the end of the full-expression established by the operand of the return statement, which, in turn, is sequenced before the destruction of local variables (8.7) of the block enclosing the return statement

So whithout copy elision, you end up copying the vector before the destruction resulting in a size of 0. With copy elision, you're inserting into the same vector you seen in the main function.

TL;DR: Don't rely on behavior that can change when NRVO is applied. These kind of behavior includes observable side effect from calling a constructor or a destructor, just like your example did.

like image 35
Guillaume Racicot Avatar answered Nov 20 '22 19:11

Guillaume Racicot