Search code examples
c++returndestructor

Different observable order of local variable destruction and its return


I have a program that behaves differently in GCC and Clang. After simplification the minimal reproducible example is

struct A {
    int i;
};

struct Finisher {
    A & a;
    ~Finisher() { a.i = 1; }
};

A f() {
    A a{0};
    Finisher fr{a};
    return a;
}

int main() {
    std::cout << f().i;
}

In GCC it prints 0 and in Clang the output is 1. Online demo: https://gcc.godbolt.org/z/oW4K93bM8

From the observed program behavior, it looks like Clang first performs ~Finisher(), which sets a.i=1, and only then return a. And GCC seemingly chooses the opposite order of operations: first returns a.i=0, and only then executes the destructor of fr.

Is there any undefined behavior in the program? And is it possible to modify Finisher in a way to always change the value of a just before returning it from the function (to return 1)?


Solution

  • It appears GCC does not perform named return value optimization (NRVO) for some aggregates.

    If you add the option -fno-elide-constructors to clang, it will give you the same behavior (no NRVO). You can also return std::move(a) to stop NRVO. If you add constructors to A (like below), both compilers give the same result: they perform NRVO (unless you stop it). Same thing if you add a large member to A (like int arr[100];).

    struct A {
        A(int i) : i(i) {}
        A(A&& o) : i(o.i) { std::cout << "A(A&&)\n"; }
    
        int i;
    };
    

    NRVO is a form of copy elision, which is one of the only two optimizations allowed to have observable side effects (like make a program print something different). So the program is not ill-formed (no undefined behavior), but it exhibits unspecified behavior.

    I am not aware of any way to modify only Finisher to make both compilers do the same thing. An easy fix is to put fr in an inner scope, but I assume you want Finisher to work like a scope guard, which should work in the function's main scope... I believe this issue is one of the reason why scope_exit is not yet in the standard library. As you can read in the "Notes" of this page:

    If the EF stored in a scope_exit object refers to a local variable of the function where it is defined, e.g., as a lambda capturing the variable by reference, and that variable is used as a return operand in that function, that variable might have already been returned when the scope_exit's destructor executes, calling the exit function. This can lead to surprising behavior.