Why does an explicitly defaulted destructor disable default move constructor?
Asked Answered
S

1

6

Why does an explicitly defaulted destructor disable default move constructor in the class? I know that it does, as explained in several existing answers. (e.g. Explicitly defaulted destructor disables default move constructor in a class )

I want to know why: what is the rationale for this, when it doesn't actually do anything that implies that the move constructor might need to have custom code? In fact, we recommend that people use =default rather than an empty body because that way the compiler knows it doesn't do anything beyond the automatic actions, exactly as if it had been automatically generated without any declaration.

To be really really clear, I know why defining a destructor with your own logic in it should suppress autogeneration. I'm pointing out that =default doesn't change anything as compared with letting the compiler implicitly generated it, so that reason does not apply here.

I recall >10 years ago there was a lot of discussion on what is the right way to specify it. I don't remember, if I ever learned, why it went the way it did in the end. Does anyone know of a compelling reason why this is a specific desirable feature in itself, or some technical reason why it ought to be this way?

demonstration: https://gcc.godbolt.org/z/86WGMs7bq

#include <type_traits>
#include <utility>
#include <string>
#include <iostream>


struct C
{
    std::string s;
    C () : s{"default ctor"} {}
//    ~C() = default;  // <<< comment this line out, and we see that original.s has its contents "stolen"
                       // <<< with this dtor declared, the original string is copied.
};

int main()
{
    C original;
    original.s = "value changed";
    C other { std::move(original) };
    using std::cout;
    cout << "original now: " << original.s << '\n';
    cout << "other is: " << other.s << '\n';
}

This rule is stated in cppreference and was mentioned the other day in a C++ conference video that was just posted, which reminded me of it.

Saraband answered 2/12, 2021 at 22:3 Comment(23)
Can you provide code demonstrating this? Because I have code saying otherwise.Archipenko
@NicolBolas godbolt.org/z/fzhEPr6Y4 I also added this to the question.Flowers
@Saraband The issue there is that you made the destructor private. If you change the access to public, the class will be move constructable.Jitney
std::is_move_constructible isn't the right way to check this. From cppreference: "Types without a move constructor, but with a copy constructor that accepts const T& arguments, satisfy std::is_move_constructible."Jedediah
@Jedediah OK, I need more elaborate testing. That explains Nicol's result, though.Flowers
This is maybe a better example: godbolt.org/z/n6P13z3j3 from the assembly you can see it calls CanMove::CanMove(CanMove&&) and CannotMove::CannotMove(CannotMove const&)Jedediah
Josuttis, C++ Move semantics: The special copy member functions and the destructor disable move support. The automatic generation of special move member functions is disabled (unless the moving operations are also declared). However, still a request to move an object usually works because the copy member functions are used as a fallback (unless the special move member functions are explicitly deleted).Sarre
I made an example that shows that an effective move constructor was generated (calls the move ctor of its members), or not, depending on the presence of the declaration.Flowers
@Saraband is that one helpful? I guess you were not the first one to ask this question: https://mcmap.net/q/83371/-why-does-destructor-disable-generation-of-implicit-move-methodsSarre
@Sarre No! That other question is why having a user-supplied destructor disables the autogenerated move. I know that. The "meaning" of an implicitly generated destructor is the same as what you get with =default , so that does not apply. Is my question (e.g. 2nd paragraph) not clear? This A does not address this at all, and in fact begs this question as a corollary.Flowers
@JDługosz: An explicitly defaulted destructor does not disable copy/move. You are having a problem only because the destructor is private. That's the problem. The compiler will not let regular code destroy the type; that's why you're getting compile errors.Archipenko
@Sarre It is not "the same question" as you imply. Mine includes "explicitly defaulted", meaning not just any old destructor but this in particular, but declaring (not defining) the same thing that happens if you don't declare anything.Flowers
@NicolBolas no, look at the Godbolt link. That bad example was replaced due to feedback within minutes after the first posting. Explicit defaulted destructor does disable move (not copy) generation.Flowers
@JDługosz: Then put the code in the question, not behind a link.Archipenko
Consult this chart. A better question is why it doesn't disable the copy constructor and assignment (answer - it's deprecated).Jacquijacquie
@NicolBolas huh, you're the one that edited in the bad example in the first place! That codes does not match what the Godbolt code showed at the time you edited it.Flowers
@JDługosz: You're the one who wrote it. I merely put it where it belonged. It's on you to put the example in the text of the question. You can't blame someone else who's trying to fix your question when you change things behind their back.Archipenko
@Jacquijacquie Please, I know what the chart says: "any user-declared". I'm asking why that applies to the explicit default of a destructor that's not user-defined. Why wouldn't the chart read, "user defined or declared but not defined inside the class body"?Flowers
@JDługosz: "I'm asking why that applies to the explicit default of a destructor that's not user-defined." A better question is... why not? Your question makes the statement, "I'm pointing out that =default doesn't change anything", but it does change something: you wrote it. If you wrote it, then you wrote it for a reason. You're saying something about the type.Archipenko
@NicolBolas so noting "so I need more elaborate testing... I made an example that shows that an effective move constructor was generated" in the comments is behind your back ?Flowers
@JDługosz: Any changes going on outside of the history-tracking features of this site can reasonably be considered hidden / out of sight. The code belongs in the question on this site. If you can't put the code in the question, because you don't have permission to copy it and license it as CC BY-SA, then StackOverflow is not the correct venue for asking about it.Honeyed
@Saraband I bet that description didn't fit into the cell. :P "user-declared" is different from "user-provided" (what you call "user-defined"), and the former means exactly what you said. Cppreference mentions the difference between those terms here.Jacquijacquie
Changing the link for the GodBolt is tracked as a change here; it doesn't update the code under the same sharing ID.Flowers
O
1

I think the best rationale is consistency: an explicitly defaulted copy constructor (among other things) disables the implicit move constructor, so the simplest rule is that any declaration of a relevant special member function does so.

In turn, the reason for A(const A&)=default; to have this effect is that it adds expressive power: one can, without tedious and error-prone member initializer lists, define classes that hold onto their resources when “moved from” (to provide an exception to an otherwise sink interface). This despite the fact that =default provides no more information there than it does on a destructor: generally speaking, it makes sense to give these constructs special meaning precisely because they have no other effect. If they did have some other meaning, it wouldn’t be safe to assume that the programmer who used them wanted some other change of behavior in addition to their direct semantics.

On that subject, why encourage generally writing ~A()=default; or ~A() {}? Neither conveys any intrinsic information to the compiler, and the behavior with the implicitly declared destructor is often preferable.

Oleander answered 4/12, 2021 at 23:57 Comment(4)
I think it has something to do with the ability to define special members as A::~A() = default in a .cpp file. For example, in that case header will look like A(const A&), and the compiler can't know from the header alone if a copy constructor is defaulted or not. Hence the deletion of implicit move constructor. But it is just a wild guess...Pyramidon
@SergeyKolesnik: Such a destructor defaulted outside its class is user-provided, not just user-declared. It is utterly equivalent to A::~A() {}, although that’s of course not true for copy constructors.Oleander
Regarding the destructor, using {} instead of =default makes a difference as the former won't be "trivial". We encourage writing the latter because of that; if you are asking why the empty body doesn't make it trivial too, that's an interesting question.Flowers
@JDługosz: Yes, ~A()=default; is better than ~A() {}, not because “the compiler knows” more, but just because the rules notice the difference. My point is that you shouldn’t be writing either one—the implicitly declared destructor is better for the same reason.Oleander

© 2022 - 2024 — McMap. All rights reserved.