While fiddling around with copy elision I came across this strange behavior:
class Obj {
public:
Obj() = default;
Obj(Obj&&) = delete;
Obj(const Obj&) { std::cout << "Copy" << std::endl; }
};
Obj f1() {
Obj o;
return o; // error C2280: move constructor is deleted
}
Obj f2() {
Obj o;
return Obj(o); // this however works fine
}
int main() {
Obj p = f1();
Obj q = f2();
return 0;
}
GCC and Clang accept this code and are able to use copy elision in both cases.
In f1()
MSVC complains that it cannot return o
because the move constructor of Obj
is deleted. However, I would expect it to be able to fall back on the copy constructor. Is this a bug in MSVC or is this desired behavior (that I don't understand) and GCC / Clang are too permissive?
If I provide a move constructor, MSVC is able to elide the move when compiling as Release.
Interestingly MSVC is able to compile f2()
. As far as I understand this is due to the mandatory copy elision when the result of a constructor call is returned. However it feels counter-intuitive that I am only able to return o
by-value if I manually copy it.
I know that this situation might not be relevant for practical use since copyable objects usually are also moveable, but I am interested in the underlying mechanics.
Here is the online example for testing: https://godbolt.org/z/sznds7
The lack of error on f1()
is a bug in both clang and gcc. It is fixed in clang's tip-of-trunk.
f1()
is not eligible for mandatory copy elision.
Deleted functions participate in overload resolution. If they are chosen as the best overload, the program is ill-formed. In f1()
, the deleted move constructor is chosen by overload resolution.
In f2()
, as of C++17, copy/move elision is guaranteed, and thus overload resolution on the move/copy constructors is not done. In C++11/14, f2()
is also an error (same error as f1()
) because copy/move elision is not guaranteed.
Also see this guideline: Never delete the special move members, which admittedly was written prior to C++17.
oh, I feel ashamed, I just realized that the other answer is by Howard Hinnant, the one person whose writings made me understand what I am painfully trying to explain here, it's a bit ridiculous...
Since copy and move constructors are both declared, they both exist. Especially here, you took care to define yourself the copy constructor; without that it would have been defined deleted (see p28 of this presentation).
The deleted aspect is just detail about the definition, but they are both actually declared then eligible to overload resolution.
In f1()
if the copy elision occurs, then there is no need to choose between copy and move constructor; none of these is used.
On the other hand, if the copy elision does not occur, then the best overload has to be chosen to construct the result; here this is the move constructor because it exists (it is declared, see here), and finally the definition is discovered as deleted, but it is too late, the choice is already made.
In f2()
, an explicit copy is explicitly requested, then the copy constructor is the best choice.
Which is quite confusing, is that when we read =delete
we think « this cannot be chosen in overload resolution » but this is wrong; =delete
is only considered after overload resolution when it is too late to find a better match.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With