4

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.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
JDługosz
  • 5,592
  • 3
  • 24
  • 45
  • 3
    Can you provide code demonstrating this? Because [I have code saying otherwise](https://gcc.godbolt.org/z/dfeerh1GT). – Nicol Bolas Dec 02 '21 at 22:06
  • @NicolBolas https://godbolt.org/z/fzhEPr6Y4 I also added this to the question. – JDługosz Dec 02 '21 at 22:11
  • 2
    @JDługosz The issue there is that you made the destructor private. If you change the access to public, the class will be move constructable. – Brian61354270 Dec 02 '21 at 22:13
  • 1
    `std::is_move_constructible` isn't the right way to check this. From [cppreference](https://en.cppreference.com/w/cpp/types/is_move_constructible): "Types without a move constructor, but with a copy constructor that accepts const T& arguments, satisfy std::is_move_constructible." – Kevin Dec 02 '21 at 22:14
  • @Kevin OK, I need more elaborate testing. That explains Nicol's result, though. – JDługosz Dec 02 '21 at 22:19
  • 2
    This is maybe a better example: https://godbolt.org/z/n6P13z3j3 from the assembly you can see it calls `CanMove::CanMove(CanMove&&)` and `CannotMove::CannotMove(CannotMove const&)` – Kevin Dec 02 '21 at 22:29
  • 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).` – alagner Dec 02 '21 at 22:35
  • 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. – JDługosz Dec 02 '21 at 22:35
  • 2
    @JDługosz is that one helpful? I guess you were not the first one to ask this question: https://stackoverflow.com/a/50490555/4885321 – alagner Dec 02 '21 at 22:40
  • @alagner 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. – JDługosz Dec 03 '21 at 15:16
  • @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. – Nicol Bolas Dec 03 '21 at 15:21
  • @alagner 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. – JDługosz Dec 03 '21 at 15:21
  • @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. – JDługosz Dec 03 '21 at 15:24
  • @JDługosz: Then put the code in the question, not behind a link. – Nicol Bolas Dec 03 '21 at 15:25
  • Consult [this chart](https://i.stack.imgur.com/HeDld.png). A better question is why it doesn't disable the copy constructor and assignment (answer - it's deprecated). – HolyBlackCat Dec 03 '21 at 15:26
  • @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. – JDługosz Dec 03 '21 at 15:26
  • 1
    @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. – Nicol Bolas Dec 03 '21 at 15:28
  • @HolyBlackCat 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"? – JDługosz Dec 03 '21 at 15:29
  • @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. – Nicol Bolas Dec 03 '21 at 15:34
  • @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_ ? – JDługosz Dec 03 '21 at 15:34
  • 1
    @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. – Ben Voigt Dec 03 '21 at 15:43
  • @JDługosz 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](https://en.cppreference.com/w/cpp/language/aggregate_initialization). – HolyBlackCat Dec 03 '21 at 16:04
  • Changing the link for the GodBolt is tracked as a change here; it doesn't update the code under the same sharing ID. – JDługosz Dec 06 '21 at 15:34

1 Answers1

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.

Davis Herring
  • 36,443
  • 4
  • 48
  • 76
  • 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... – Sergey Kolesnik Dec 05 '21 at 01:03
  • @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. – Davis Herring Dec 05 '21 at 04:24
  • 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. – JDługosz Dec 06 '21 at 15:32
  • @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. – Davis Herring Dec 06 '21 at 15:49