23

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

cigien
  • 57,834
  • 11
  • 73
  • 112
Devon Cornwall
  • 957
  • 1
  • 7
  • 17
  • 1
    In f1 copy elision is optional in f2 not. Msvc is right but gcc and clang too. See https://en.cppreference.com/w/cpp/language/copy_elision – Bernd Feb 27 '21 at 23:42
  • @Bernd I understand that part. But the core of the question remains: Why does MSVC not just use the copy constructor (without optimizing anything)? – Devon Cornwall Feb 27 '21 at 23:44
  • My understanding is that if the move constructor is selected by overload resolution and it is deleted, compiling should fail whether or not it would be elided. – doug Feb 27 '21 at 23:49
  • Declaring a function deleted is not the same as omitting it entirely. A deleted function still participates in overload resolution, and if actually chosen, the program is ill-formed. Just drop the move constructor; the compiler will not implicitly declare it when a user-defined copy constructor exists. – Igor Tandetnik Feb 27 '21 at 23:49
  • 1
    While `delete`d functions do participate in overload resolution, there appears to be an [exception](http://eel.is/c++draft/over.match#funcs.general-9) for special member functions, where they are explicitly ignored. Looks like a MSVC bug to me. – cigien Feb 27 '21 at 23:54
  • 3
    @cigien - does that apply? this move constructor isn't a _defaulted_ move (`= default`) ... – davidbak Feb 27 '21 at 23:56
  • It should also be noted that a type with a copy constructor but *no* move constructor is a kind of broken type. There's (almost) no reason to create such a type, as the move constructor could just do what the copy constructor does. – Nicol Bolas Feb 27 '21 at 23:57
  • That's not exactly right. The quote is "A defaulted move special member function ([class.copy.ctor], [class.copy.assign]) that is defined as deleted is excluded from the set of candidate functions in all contexts." So this covers `=delete` I think. – cigien Feb 27 '21 at 23:57
  • 1
    @cigien - but in the example given in that section the move constructor being discussed, for `struct B`, is `= default`, but it is "defined" deleted because `struct B` contains a member which is a struct which has a deleted (`= delete`) move constructor ... – davidbak Feb 27 '21 at 23:59
  • Hmm, ok, I may be misunderstanding what "defined as deleted" means in that quote. – cigien Feb 28 '21 at 00:01
  • 1
    @cigien When a special member function is declared as defaulted, the compiler provides an implicit definition following a set of rules. Sometimes, these rules say that the function should be deleted (e.g. a move constructor for a class that has a non-movable member); then the function is "defined as deleted". In this case, the overload resolution behaves as if the function weren't declared at all. – Igor Tandetnik Feb 28 '21 at 00:29
  • @IgorTandetnik Ah, that makes a lot more sense now. I was clearly misreading that text. Thanks for the explanation. – cigien Feb 28 '21 at 00:37

2 Answers2

26

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.

Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
  • I'm a bit confused on part of this, it sounds like you're saying that GCC/Clang resolved to the deleted move constructor. Is that A) the bug in the compiler, B) defined behavior in the spec, or C) undefined behavior (making it not a bug)? It sounds like you're saying C, so `f1` would always have undefined behavior with objects that have a deleted move constructor. If that's the case, was the bug that GCC/Clang defined a move constructor without a copy constructor? Is it a bug if it's UB? – jrh Feb 28 '21 at 15:46
  • The first sentence in the answer is the entire answer. Everything else is supporting information for the first sentence. – Howard Hinnant Feb 28 '21 at 16:09
  • 2
    @jrh Ill formed programs are not UB. Ill formed programs require a diagnostic (an error). Ill formed programs, no diagnostic required, can compile and generate UB, but this isn't a ill-formed NDR case; last I counted, you could count ill-formed NDR cases on one hand. – Yakk - Adam Nevraumont Mar 01 '21 at 02:01
  • Thanks @Yakk-AdamNevraumont. You understood jrh's question where I did not. Good answer. After the decades roll by in this line of work, I've fallen into the trap of mistaking standards-speak for plain english. – Howard Hinnant Mar 01 '21 at 03:35
12

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.

prog-fh
  • 13,492
  • 1
  • 15
  • 30
  • 4
    Not so much ridiculous as it is funny, looks like you got some tuff competition ;) I do appreciate your effort so it's an upvote for me. – anastaciu Feb 28 '21 at 00:29
  • 6
    As anastaciu says, it's more amusing than anything else. It's *definitely* nothing to be ashamed of. We all learn from each other; that's just how it goes :) – cigien Feb 28 '21 at 00:35
  • 3
    I actually like this answer better since it explains the overload resolution of the deleted method in far more detail – Mooing Duck Feb 28 '21 at 19:12