My understanding is that in C++17, the following snippet is intended to Do The Right Thing:
struct Instrument; // instrumented (non-trivial) move and copy operations
struct Base {
Instrument i;
};
struct Derived : public Base {};
struct Unrelated {
Instrument i;
Unrelated(const Derived& d): i(d.i) {}
Unrelated(Derived&& d): i(std::move(d.i)) {}
};
Unrelated test1() {
Derived d1;
return d1;
}
Base test2() {
Derived d2;
return d2; // yes, this is slicing!
}
That is, in C++17, the compiler is supposed to treat both d1
and d2
as rvalues for the purposes of overload resolution in those two return statements. However, in C++14 and earlier, this was not the case; the lvalue-to-rvalue transformation in return
operands was supposed to apply only when the operand was exactly the correct return type.
Furthermore, GCC and Clang both appear to have confusing and possibly buggy behavior in this area. Trying the above code on Wandbox, I see these outputs:
GCC 4.9.3 and earlier: copy/copy (regardless of -std=)
Clang 3.8.1 and earlier: copy/copy (regardless of -std=)
Clang 3.9.1 and later: move/copy (regardless of -std=)
GCC 5.1.0 through 7.1.0: move/copy (regardless of -std=)
GCC 8.0.1 (HEAD): move/move (regardless of -std=)
So this started out as a tooling question and ended up with a side order of "what the heck is the correct behavior for a C++ compiler?"
My tooling question is: In our codebase, we have several places that say return x;
but that accidentally produces a copy instead of a move because our toolchain is GCC 4.9.x and/or Clang. We'd like to detect this situation automatically and insert std::move()
as needed. Is there any easy way to detect this issue? Maybe a clang-tidy check or a -Wfoo
flag we could enable?
But of course now I'd also like to know what is the correct behavior of a C++ compiler on this code. Are these outputs indicative of GCC/Clang bugs? Are they being worked on? And is the language version (-std=
) supposed to matter? (I'd think that it is supposed to matter, unless the correct behavior has been updated via Defect Reports going all the way back to C++11.)
Here is a more complete test inspired by Barry's answer. We test six different cases where lvalue-to-rvalue conversion would be desirable.
GCC 4.9.3 and earlier: elided/copy/copy/copy/copy/copy
Clang 3.8.1 and earlier: elided/copy/copy/copy/copy/copy
Clang 3.9.1 and later: elided/copy/move/copy/copy/copy
GCC 5.1.0 through 7.1.0: elided/copy/move/move/move/move
GCC 8.0.1 (HEAD): elided/move/move/move/move/move
ICC 17: elided/copy/copy/copy/copy/copy
ICC 18: elided/move/move/move/copy/copy
MSVC 2017 (wow): elided/copy/move/copy/copymove/copymove
After Barry's answer, it seems to me that Clang 3.9+ does the technically correct thing in all cases; GCC 8+ does the desirable thing in all cases; and in general I ought to stop teaching that people "just return x
and let the compiler DTRT" (or at least teach it with a huge flashing caveat) because in practice the compiler will not DTRT unless you are using a bleeding-edge (and technically non-conforming) GCC.
2023 update
Since I submitted this question in 2018, C++ has evolved (in this area mainly due to two papers by me and one by David Stone), such that the paper and practical behaviors are much more in keeping with what I originally expected. The more complete test above now produces moves on the most recent GCCs and Clangs, in all language modes:
GCC 8, all stds: elided/move/move/move/move/move
GCC 9-10, all stds: elided/copy/move/move/move/move
GCC 12, -std < 20: elided/copy/move/copy/move/move
GCC 12, -std >= 20: elided/move/move/move/move/move
GCC 13+, all stds: elided/move/move/move/move/move
Clang 12, all stds: elided/copy/move/copy/copy/copy
Clang 13, all stds: elided/move/move/move/move/move