2022-01-31

What does explicitly-defaulted move constructor do?

I'm new to C++ and need some help on move constructors. I have some objects that are move-only, every object has different behaviours, but they all have a handle int id, so I tried to model them using inheritance, here's the code

#include <iostream>
#include <vector>

class Base {
  protected:
    int id;

    Base() : id(0) { std::cout << "Base() called " << id << std::endl; }
    virtual ~Base() {}
    Base(const Base&) = delete;
    Base& operator=(const Base&) = delete;
    Base(Base&& other) noexcept = default;
    Base& operator=(Base&& other) noexcept = default;
};

class Foo : public Base {
  public:
    Foo(int id) {
        this->id = id;
        std::cout << "Foo() called " << id << std::endl;
    }
    ~Foo() { std::cout << "~Foo() called " << id << std::endl; }
    
    Foo(const Foo&) = delete;
    Foo& operator=(const Foo&) = delete;
    Foo(Foo&& other) noexcept = default;
    Foo& operator=(Foo&& other) noexcept = default;
};

int main() {
    std::vector<Foo> foos;

    for (int i = 33; i < 35; i++) {
        auto& foo = foos.emplace_back(i);
    }

    std::cout << "----------------------------" << std::endl;
    return 0;
}

Each derived class has a specific destructor that destroys the object using id (if id is 0 it does nothing), I need to define it for every derived type. In this case, the compiler won't generate implicitly-declared copy/move ctors for me, so I have to explicitly make it move-only to follow the rule of five, but I don't understand what the =default move ctor does.

When the second foo(34) is constructed, vector foos reallocates memory and moves the first foo(33) to the new allocation, however, I saw that both the source and target of this move operation has an id of 33, so after the move, foo(33) is destroyed, leaving an invalid foo object in the vector. In the output below, I also didn't see a third ctor call, so what on earth is foo(33) being swapped with? a null object that somehow has an id of 33? Where does that 33 come from, from copy? but I've explicitly deleted copy ctor.

Base() called 0
Foo() called 33
Base() called 0
Foo() called 34
~Foo() called 33  <---- why 33?
----------------------------
~Foo() called 33
~Foo() called 34

Now if I manually define the move ctor instead:

class Foo : public Base {
  public:
    ......

    // Foo(Foo&& other) noexcept = default;
    // Foo& operator=(Foo&& other) noexcept = default;

    Foo(Foo&& other) noexcept { *this = std::move(other); }
    Foo& operator=(Foo&& other) noexcept {
        if (this != &other) {
            std::swap(id, other.id);
        }
        return *this;
    }
}
Base() called 0
Foo() called 33
Base() called 0
Foo() called 34
Base() called 0  <-- base call
~Foo() called 0  <-- now it's 0
----------------------------
~Foo() called 33
~Foo() called 34

this time it's clearly swapping foo(33) with a base(0) object, after id 0 is destroyed, my foo object is still valid. So what's the difference between the defaulted move ctor and my own move ctor?

As far as I understand, I almost never need to manually define my move ctor body and move assignment operator unless I'm directly allocating memory on the heap. Most of the time I'll only be using raw data types such as int, float, or smart pointers and STL containers that support std::swap natively, so I thought I would be just fine using =default move ctors everywhere and let the compiler does the swap memberwise, which seems to be wrong? Perhaps I should always define my own move ctor for every single class? How can I ensure the swapped object is in a clean null state that can be safely destructed?



from Recent Questions - Stack Overflow https://ift.tt/NW4T5qQOf
https://ift.tt/Mhf8FyP6J

No comments:

Post a Comment