12
votes

What is the rationale behind the different treatment of implicitly and explicitly deleted move constructors in the C++11 standard, with respect to the implicit generation of move constructors of containing/inheriting classes?

Do C++14/C++17 change anything? (Except DR1402 in C++14)

Note: I understand what is happening, I understand that it is according to the C++11 standard's rules, I'm interested in the rationale for these rules that imply this behavior (please make sure not to simply restate that it is the way it is because the standard says so).


Assume a class ExplicitDelete with an explicitly deleted move ctor and an explicitly defaulted copy ctor. This class isn't move constructible even though a compatible copy ctor is available, because overload resolution chooses the move constructor and fails at compile time due to its deletion.

Assume a class ImplicitDelete which either contains or inherits from ExplicitDelete and does nothing else. This class will have its move ctor implicitly declared as deleted due to C++11 move ctor rules. However, this class will still be move constructible via its copy ctor. (Does this last statement have to do with resolution of DR1402?)

Then a class Implicit containing/inheriting from ImplicitDelete will have a perfectly fine implicit move constructor generated, that calls ImplicitDelete's copy ctor.

So what is the rationale behind allowing Implicit to be able to move implicitly and ImplicitDelete not to be able to move implicitly?

In practice, if Implicit and ImplicitDelete have some heavy-duty movable members (think vector<string>), I see no reason that Implicit should be vastly superior to ImplicitDelete in move performance. ImplicitDelete could still copy ExplicitDelete from its implicit move ctor—just like Implicit does with ImplicitDelete.


To me, this behavior seems inconsistent. I'd find it more consistent if either of these two things happened:

  1. The compiler treats both the implicitly and explicitly deleted move ctors the same:

    • ImplicitDelete becomes not move-constructible, just like ExplicitDelete
    • ImplicitDelete's deleted move ctor leads to a deleted implicit move ctor in Implicit (in the same way that ExplicitDelete does that to ImplicitDelete)
    • Implicit becomes not move-constructible
    • Compilation of the std::move line utterly fails in my code sample
  2. Or, the compiler falls back to copy ctor also for ExplicitDelete:

    • ExplicitDelete's copy constructor is called in all moves, just like for ImplicitDelete
    • ImplicitDelete gets a proper implicit move ctor
    • (Implicit is unchanged in this scenario)
    • The output of the code sample indicates that the Explicit member is always moved.

Here's the fully working example:

#include <utility>
#include <iostream>
using namespace std;

struct Explicit {
    // prints whether the containing class's move or copy constructor was called
    // in practice this would be the expensive vector<string>
    string owner;
    Explicit(string owner) : owner(owner) {};
    Explicit(const Explicit& o) { cout << o.owner << " is actually copying\n"; }
    Explicit(Explicit&& o) noexcept { cout << o.owner << " is moving\n"; }
};
struct ExplicitDelete {
    ExplicitDelete() = default;
    ExplicitDelete(const ExplicitDelete&) = default;
    ExplicitDelete(ExplicitDelete&&) noexcept = delete;
};
struct ImplicitDelete : ExplicitDelete {
    Explicit exp{"ImplicitDelete"};
};
struct Implicit : ImplicitDelete {
    Explicit exp{"Implicit"};
};

int main() {
    ImplicitDelete id1;
    ImplicitDelete id2(move(id1)); // expect copy call
    Implicit i1;
    Implicit i2(move(i1)); // expect 1x ImplicitDelete's copy and 1x Implicit's move
    return 0;
}
1
I'm not sure I follow exactly but what would be the point of declaring a function deleted in the first place if the compiler would just get clever finding ways to avoid calling it?5gon12eder
Just to clarify, I am not assuming the rules are bad and I know better, but rather, that with my limited knowledge I do not understand the reasons for current behavior -- it seems inconsistent in the context of what I know. I have added a small section just before the code sample to answer your question.Irfy
My first impression was that your alternative behaviour (1) made the most sense; however after further thought, it does make sense that Implicit on your example can answer a move-request by moving the movable parts, and copying the non-movable parts.M.M
@M.M.: Exactly. I'd prefer (2) because it allows for more efficient automatically generated code without any apparent deficits. -- However, I do not agree with your statement that ImplicitDelete can move implicitly. It is move-constructible, no doubt, but its implicit move ctor is deleted, as per the rule "T has direct or virtual base class with a deleted or inaccessible destructor". It move-constructs via its implicit copy ctor -- and that is my point, ExplicitDelete should too.Irfy
My guess for this behavior is that by explicitly deleteing something you're saying "it's a bad thing if I get selected, so slap my hand if that happens". But with an explicitly defaulted constructor, ImplicitDelete shouldn't have to worry about all of its base classes and data members, so in that case if the move constructor would be ill-formed, it's simply ignored.Praetorian

1 Answers

6
votes

So what is the rationale behind allowing Implicit to be able to move implicitly and ImplicitDelete not to be able to move implicitly?

The rationale would be this: the case you describe does not make sense.

See, all of this started because of ExplicitDelete. By your definition, this class has an explicitly deleted move constructor, but a defaulted copy constructor.

There are immobile types, with neither copy nor move. There are move-only types. And there are copyable types.

But a type which can be copied but has an explicitly deleted move constructor? I would say that such a class is a contradiction.

Here are the three facts, as I see it:

  1. Explicitly deleting a move constructor is supposed to mean you can't move it.

  2. Explicitly defaulting a copy constructor is supposed to mean you can copy it (for the purposes of this conversation, of course. I know you can still do things that make the explicit default deleted instead).

  3. If a type can be copied, it can be moved. That's why the rule about implicitly deleted move constructors not participating in overload resolution exists. Therefore, movement is a proper subset of copying.

The behavior of C++ in this instance is inconsistent because your code is contradictory. You want your type to be copyable but not moveable; C++ does not allow that, so it behaves oddly.

Look at what happens when you remove the contradiction. If you explicitly delete the copy constructor in ExplicitDelete, everything makes sense again. ImplicitDelete's copy/move constructors are implicitly deleted, so it is immobile. And Implicit's copy/move constructors are implicitly deleted, so it too is immobile.

If you write contradictory code, C++ will not behave in an entirely legitimate fashion.