3

I have the following code:

#include <iostream>

class B{
public:
    //this should all be generated by default but
    //throwing them in here just to be sure
    B() = default;
    B(const B& b) = default;
    B& operator=(const B& b) = default;
    B(B&& b) = default;
    B& operator=(B&& b) = default;
};

class A {
public:
    A(B x) : x_(x) {}
    A(const A& a) = delete;
    A& operator=(const A& a) = delete;
    //move operations should be generated by compiler?
private:
    B x_;
};

int main() {
    A a = A(B());
}

I'm expecting this to compile and an A to be created using it's move constructor, but instead this fails with the following message:

error: use of deleted function ‘A::A(const A&)’ A a = A(B()); note: declared here A(const A& a) = delete;

Of course adding the move operations and marking them with the default keywords eliminates the problem. Should I assume that the move operations were not generated by the compiler, why is that? Does delete keyword mark the methods as user-implemented so no move operations are generated? Why is the copy constructor prefered instead? I'm using gcc to compile.

Deduplicator
  • 44,692
  • 7
  • 66
  • 118
cuv
  • 1,159
  • 2
  • 10
  • 20

2 Answers2

4

If you provide implementation of copy constructor/operator=, move operations are no longer generated by default. If you want them, you need to explicitly tell that you do.

As per cppreference:

If no user-defined move constructors are provided for a class type (struct, class, or union), and all of the following is true:

  • there are no user-declared copy constructors;
  • there are no user-declared copy assignment operators;
  • there are no user-declared move assignment operators;
  • there are no user-declared destructors;

then the compiler will declare a move constructor as a non-explicit inline public member of its class with the signature T::T(T&&).

Here's a helpful chart for this kind of situation:

enter image description here attribution

krzaq
  • 16,240
  • 4
  • 46
  • 61
  • But i thought i didn't. I just deleted all copy operations from class A. Is that considered user-implementation? – cuv Nov 01 '16 at 10:45
  • 1
    @cuvidk deleted functions take part in overload resolution, so yeah, it counts as defined (as deleted) - to the best of my understanding. I'll try to hunt the wording for this – krzaq Nov 01 '16 at 10:48
  • 1
    I consider your answer extremely helpful. The above table clears things up if your caught in the constructor-chaos as I am. I tried to search if keywords as delete/default count as user-defined but couldn't find answers. – cuv Nov 01 '16 at 10:51
  • But if deleting the copy operations is considered user-defined implementation and so as a consequence the move operations are deleted, why is replacing `A a = A(B());` with `A a(A(B()));` working? – cuv Nov 01 '16 at 11:00
  • 2
    @cuvidk try using `a` ;) You've hit Most Vexing Parse. (`a` is a function returning `A` and taking function returning `A` and taking function returning `B` and taking no arguments) – krzaq Nov 01 '16 at 11:03
  • 2
    (if it helps) Here is a video description of a transposed version of the above chart: http://www.youtube.com/watch?v=vLinb2fgkHk&t=23m20s – Howard Hinnant Nov 01 '16 at 13:59
  • @HowardHinnant thank you, it was illuminating. Am I correct to say that the chart from this answer isn't entirely truthful about move ops when a copy operation is declared? (according to your chart, they're not declared + overload will select copy ops) – krzaq Nov 01 '16 at 18:09
  • 1
    @krzaq: I think the observable result is the same either way: overload resolution will select copying (assuming the user doesn't declare move ops). – Howard Hinnant Nov 01 '16 at 20:33
2

Saying = delete you're not just saying that the code is not present, but that if code tries to call that you should get a compile error.

More specifically deleted members are not simply removed from the set of available options: they are searched and if they match you get a compile error. In other words = delete is meant for signatures that when match imply a usage problem, not for signature you don't want to match.

For example deleted members do indeed participate in overload resolution.

Providing = delete is exactly like providing an implementation, with the difference that if the compiler ends up generating code that would call the deleted function then you have a compile error. If you provide a deleted copy constructor you still provided a user defined copy constructor.

Another subtle point you should pay attention to is that automatic generation of move constructor/assignment depends for example on the presence of a "user defined copy constructor" and that definition as a strict meaning. Something that is not logically obvious (at least wasn't for me) is that a template matching the signature of a copy constructor is not a copy constructor. In other words in:

struct Foo {
    template<typename T>
    Foo(const T& other) { ... }
};

the template can match Foo(const Foo&) but is not considered a user-defined copy constructor (don't look for a deep reason, it's this way because the standard says so) and therefore you still get implicitly generated move constructors. If the default implementation is not correct this will be a source of serious problems...

Community
  • 1
  • 1
6502
  • 112,025
  • 15
  • 165
  • 265