-2

I'm having trouble understanding why my declaring a destructor in my class doesn't delete the implicitly declared move constructor as is specified in this documentation, where it says :

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 is no user-declared destructor.

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

Note that the the copy constructor, copy assignment and move assignment should all be deleted too if I define my own destructor (hence the rule of 5)

Here's a small code sample to demonstrate my point :

class Test
{
public:
    ~Test() {}
        
protected:
    int a = 5;      
};

void main()
{
    Test t1;
    Test t2 = std::move(t1); //shouldn't work (note : if we have a copy constructor, will work even if the move constructor doesn't exist)
}

What am I missing? I'm sure it's obvious but I can't seem to find the documentation that explains the aforementioned behavior. I run my code on Visual Studio 2022 with C++20.

I discovered the aforementioned behaviour after having to create a virtual destructor in one of my base class and realizing that I didn't have to redefine all the copy&move constructors/assignments as I thought I'd have to.

Also, it's not 100% clear to me, why defaulting any of the copy/move constructors/assignments specifically with the keyword default requires in theory to redefine them all (+destructor) ? What's the incentive behind that choice if it's just defaulted ?

Thanks in advance.

David G
  • 94,763
  • 41
  • 167
  • 253
Getter
  • 765
  • 2
  • 6
  • 15
  • 1
    The Rule of 5 is a rule for you, the developer, to follow. Your compiler is not compelled to follow or enforce this rule. The rule exists specifically _because_ your compiler will sometimes make wrong assumptions. – Drew Dormann Apr 13 '23 at 13:27
  • "should all be deleted too if I define my own destructor (hence the rule of 5)" this is a misunderstanding. The rule says that if you implement one then you must implement all. Or more generally, if you manage a resource you need to implement them, *because* the compiler generated ones will be wrong. – 463035818_is_not_an_ai Apr 13 '23 at 13:30
  • in other words, the rules by which the compiler generates the special members is somewhat unrelated to the rule of 5, because for the rule of 5 what counts is the ones you implement – 463035818_is_not_an_ai Apr 13 '23 at 13:32
  • I think you've *misapprehended* what `std::move(t1)` means. It does not mean **move** this object. It casts the object so as to be *move-able*. If the object does not have a move-constructor, then the compiler will use the copy-constructor. (Ditto with move-assignment and copy-assignment situations.) – Eljay Apr 13 '23 at 13:51
  • Yeah I know that Eljay, what I didn't know was that the copy constructor wasn't default deleted after specifying a destructor as I thought it was. – Getter Apr 13 '23 at 13:56
  • Btw, the two possible duplicates that were posted above have nothing to do with my question (which is not about the use of the copy constructor when there is no move constructor, nor about std::move, but about whether or not the move constructor/assignment are default deleted from the 'test class' above because I defined a destructor? – Getter Apr 13 '23 at 13:58
  • 2
    You may want to bookmark [special members](https://howardhinnant.github.io/smf.jpg) from Howard Hinnant. – Eljay Apr 13 '23 at 14:10
  • Try to focus on asking only one clear, focused question. The (primary?) question here - "What am I missing?" leaves a lot of room for interpretation. If you feel that the duplicates aren't answering your question, that is because we are guessing. – Drew Dormann Apr 13 '23 at 14:33

1 Answers1

2

Yes. The docs you quote are correct. There is no move constructor generated by the compiler, because you declared a destructor.

What you observe is not a contradiction to the documentation you quote. However, there are more special members that get generated by the compiler. From cppreference:

If no user-defined copy constructors are provided for a class type (struct, class, or union), the compiler will always declare a copy constructor as a non-explicit inline public member of its class.

And thats what you see. Test t2 = std::move(t1); calls the copy constructor.

If you delete the copy constructor then there is no move constructor generated too:

#include <utility>

class Test
{
public:
    ~Test() {}
    Test() = default;
    Test(const Test&) = delete;    
protected:
    int a = 5;      
};

int main()
{
    Test t1;
    Test t2 = std::move(t1); 
}

results in:

<source>: In function 'int main()':
<source>:16:27: error: use of deleted function 'Test::Test(const Test&)'
   16 |     Test t2 = std::move(t1); //shouldn't work (note : if we have a copy constructor, will work even if the move constructor doesn't exist)
      |                           ^
<source>:8:5: note: declared here
    8 |     Test(const Test&) = delete;
      |     ^~~~

I can only try to explain you the reasoning behind this. I think it is largely historic. From my limited understanding I would say that the original rules are overly optimistic in letting the compiler generate the special members. Often they don't do the right thing.

Consider how things were before move semantics. The rule of 3 says that if you implement any of the special members you need to define them all. This is not reflected by the original rules of when the compiler generates them. The copy constructor is generated even when you implement a custom destructor. And this is one major reason why we need the rule of 3, because the rules for when the compiler generates the 3 was a little too optimistic and often results in broken code (when the rule is not followed by the programmer. It is not followed by the compiler).

Now with move semantics things got more "right". The rule of 5 says that if you implement one you probably also need to implement the others. And that is now also reflected in the rules of when the compiler generates the move constructor.

463035818_is_not_an_ai
  • 109,796
  • 11
  • 89
  • 185
  • 1
    The "reasoning behind this" - I believe Scott Meyers was instrumental - was the discovery that certain valid C++03 classes could break if a C++11 compiler auto-generated move operations. – Drew Dormann Apr 13 '23 at 13:54
  • 1
    @DrewDormann Its too sad he retired. Soon nobody will understand c++ anymore because we have nobody to explain it – 463035818_is_not_an_ai Apr 13 '23 at 13:58
  • Thanks for your explanation. I hadn't understood that the default copy constructor was still present in my class after defining my own destructor (I had read otherwise…). However, my question about whether the move constructor is still present still remains somehow (from the doc I posted, I guess not). And how can I test that enforcing the move constructor is called and not the copy constructor in case there is no move constructor? Thanks. – Getter Apr 13 '23 at 14:02
  • @Getter the docs you quote are correct. There is no move constructor. Though for your `Test` class there is literally 0 difference between moving it or copying it. I do not understand what you mean with "how can I test that enforcing the move constructor is called and not the copy constructor in case there is no move constructor" When there is no move constructor, then there is nothing to be enforced, it cannot be called – 463035818_is_not_an_ai Apr 13 '23 at 14:05
  • Ofc, it doesn't make much sense with such a basic class. However, I'm building rn a rather big class for a project and I'd have liked to know on that early development stage whether it has or not move constructor (&assignment). As std::move will use copy if no move constructor exists (without throwing a warning or anything), how will I ever know it doesn't have one ? Maybe I'm worrying too much about something that's not important, but I'd just like to understand this thoroughly (I'm not too familiar with all this…) – Getter Apr 13 '23 at 14:10
  • @Getter its not about basic vs big but about whether it has members that can be moved. You can have a large sized class that cannot be moved and a class that has only a single pointer as member that is a perfect use case for moving. – 463035818_is_not_an_ai Apr 13 '23 at 14:12
  • @Getter `std::move` will not move or copy anything. `std::move` is just a cast – 463035818_is_not_an_ai Apr 13 '23 at 14:13
  • I mean, to be more accurate, I know now there is no move constructor/assignment in my class, but how can I *test* it with a piece of code? – Getter Apr 13 '23 at 14:13
  • @Getter I am not sure what problem you see. If you write the class you should know if it has a move constructor. Either you implement it or you declare it as `= default` then you can be sure that there is one. If you are just using the type you can try to move it and the worst that happens is that if falls back to making a copy. – 463035818_is_not_an_ai Apr 13 '23 at 14:14
  • @Getter that is a valid new question you could post. Your question here does not ask how to do this test. – Drew Dormann Apr 13 '23 at 14:14
  • @Getter well, given some `T` how to test whether it can be moved is a good question actually. `std::is_move_constructible` is true also for a type that can only be copy constructed. I second Drew, its a different question that you should post as separate question – 463035818_is_not_an_ai Apr 13 '23 at 14:15
  • This is of some interest : https://stackoverflow.com/questions/7054952/type-trait-for-moveable-types/27851536#27851536 Also, I was thinking : how annoying that the move constructors/assignments are deleted just because I need to define a virtual destructor… It's so unpractical… – Getter Apr 13 '23 at 14:25
  • 1
    @Getter Just don't rely on the compiler generating it implicitly but make it explicit: `Test(Test&&) = default;`. Explicit is always better than implicit – 463035818_is_not_an_ai Apr 13 '23 at 14:31