3

Given the following toy code:

class X
{
public:
    X() { }
    X(const X&) { }
    //X(X&&) = delete;
};

int main()
{
    X x;
    X y = std::move(x);
}

I know that X::X(X&&) is implicitly deleted in this case because X(const X&) exists as an user-declared constructor. But I'm a little bit confused with the meaning of the terminology "implicitly deleted" here: if we uncomment X(X&&) = delete;, the code will behave differently, which means there is a difference between implicitly deleted and "explicitly" deleted.

There are two different understandings in my mind:

  • "implicitly deleted" means that the compiler generates some code similar to X(X&&) = delete; (that is, the compiler knows that there is X(X&&) and it knows that X(X&&) is deleted), but the code generated differs from X(X&&) = delete; in a way such that when X y = std::move(x); tries to call X::X(X&&), the compiler select X(const X&) rather than reporting an error. (Had X(X&&) = delete; been uncommented, the compiler will not select X(const X&) and will report an error)

  • "implicitly deleted" means that X(X&&) is not declared in the class at all, in other words, the compiler does not have any information about X(X&&) and thus X y = std::move(x); is matched directly to X(const X&).

May I ask which of my understandings is the correct one?


My guess is that the former should be the correct one. Because if we change the above code as follows:

class X
{
public:
    X() { }
    //X(const X&) { }
    X(X&&) {}
};

int main()
{
    X x;
    X y = x;
}

We get an error saying 'X::X(const X &)': attempting to reference a deleted function which means the compiler knows the existence of X(const X &) when it is implicitly deleted.

However, for me, my latter understanding seems to be a more straightforward way of getting the work done. So I wonder why we want to design the concept of "implicitly deleted" (it also gives me a little feeling of inconsistency since "implicitly deleted" needs to act differently from "explicitly deleted")

CPPL
  • 726
  • 1
  • 10
  • You need to consider the context in which move semantics were added to the language. They had to make sure existing code would continue to work as intended, code written before anyone even thought of move semantics. I believe the reasoning is that old code that had a copy constructor and no explicit move constructor should keep working as before, by using the copy constructor even where move construction would normally be used. By adding an explicitly deleted move constructor you prove that your class is not legacy code and that you intentionally don't want move construction to be allowed. – François Andrieux Jun 06 '22 at 13:28
  • 1
    The *implicitly deleted* really means "implicitly not synthesized by the compiler". It's not the same as `X(X&&) = delete;`, because then the signature is available for matching for overload resolution, and if matched, will failed because it is explicitly deleted. I wish there was a way to write in code (rather than a comment) `X(X&&) = delete(intentional);` and yet not have it be an overload resolution. – Eljay Jun 06 '22 at 13:28

1 Answers1

6

When you uncomment X(X&&) = delete;, what you are doing is declaring that ctor as deleted. It means, for instance, that it does participate in overload resolution, which it wins in the case of X y = std::move(x);, but can't actually be used, because it lacks a body.

Note that this is something that doesn't apply specifically to (special or not) member functions, but to functions in general:

#include<iostream>

// this is akin to defining both copy and move ctor
auto f(int) { std::cout << "int" << std::endl;}
auto f(double) { std::cout << "double" << std::endl;}

// this is akin to defining copy ctor and deleting move ctor
auto g(int) { std::cout << "int" << std::endl;}
auto g(double) = delete;

// this is akin to defining only copy ctor
auto h(int) { std::cout << "int" << std::endl;}
// auto h(double) is not even delcared

int main() {
    f(1);
    f(1.2);
    g(1);
    //g(1.2); // compile time error
    h(1);
    h(1.2); // round error
}

What is specific to special member functions, is how declaring/defining/defaulting/deleteing/not-writing-at-all one influences the other.

Everything is explained at this page, but it requires a quite fine reading.

Here's a tricky bit (my bold):

Deleted implicitly-declared move constructor

The implicitly-declared or defaulted move constructor for class T is defined as deleted if any of the following is true:

  • T has non-static data members that cannot be moved (have deleted, inaccessible, or ambiguous move constructors);
  • […]

[…]

What does the above mean?

Here's an example:

// copyable but not movable
struct CNM {
    CNM() {};
    CNM(CNM const&) = default;
    CNM(CNM&&) = delete;
};

struct W {
    W() {}
    W(W&&) = default; // defaulted... but actually deleted!
    CNM nm;
};

W w1;
W w2{std::move(w1)}; // compile time error
Enlico
  • 23,259
  • 6
  • 48
  • 102