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