2

If the move constructor of your class is noexcept then std::vector will allocate new memory and then move construct your objects in the new memory. If it's not "noexcept" it will copy construct them. If it copy constructs them, then those objects still need to be destroyed before deallocating the old buffer. However why is it the case that if the object is moved it still calls the destructors on all the old objects. I don't see why this is necessary. Any destructor call on a moved-from object is not going to do anything, apart from some conditional checks. You could argue that the "object is technically not destroyed", and that's true, but since that memory is being deallocated, and the next time that memory is used the only defined way to access objects there is to construct them first, I don't see why this needs to be done:

struct Foo
{

    void* buffer;
    Foo() : buffer(new char[16]) {}
    Foo(Foo&& other) { buffer = other.buffer; if (other.buffer != nullptr) other.buffer = nullptr; }

    ~Foo()
    {
        if (buffer != nullptr) delete buffer;
    }
};

int main()
{

    

    Foo foo;
    Foo foo2 = std::move(foo);

    foo.~Foo(); /* NO EFFECT */

    /* BUT ASSUMING WE DIDN'T CALL THE CONSTRUCTOR, WE JUST CONSTRUCT OVER IT */
    new (&foo) Foo{};
    /* THEN THE OLD FOO CEASES TO EXIST EVEN IF THE DESTRUCTOR IS NEVER CALLED */

}

Here is a quick program showing that std::vector calls the destructors on the old moved-from objects:

#include <iostream>
struct Foo
{
    
    Foo() {}
    Foo(uint32 id) {  }
    Foo(const Foo& other) 
    {
        std::cout << "Copy constructor called\n";

    }
    Foo(Foo&& other) noexcept 
    {
    
        std::cout << "Move constructor called\n";
    };

    
    ~Foo()
    {
        std::cout << "Destructor called\n";
    }
};

int main()
{
    Foo foo;
    std::vector<Foo> v;
    v.push_back(std::move(foo));
    v.push_back(std::move(foo));
    v.push_back(std::move(foo));
    v.push_back(std::move(foo));

}
Zebrafish
  • 11,682
  • 3
  • 43
  • 119
  • 1
    Because the old object still exists and needs to be destroyed. When move constructors were added to the language they didn't change how destructors worked. – user253751 Jun 10 '22 at 10:23
  • @Zebrafish Moved objects stay in an unspecified but a valid state. They must be destroyed. – Vlad from Moscow Jun 10 '22 at 10:23
  • your `Foo`s destructor doesnt need to do something for a moved from object, but that doesnt imply that thats the case in general – 463035818_is_not_an_ai Jun 10 '22 at 10:24
  • for example for objects that cannot move a move is just a copy. – 463035818_is_not_an_ai Jun 10 '22 at 10:24
  • Case in point `std::vector` will just swap the data pointers when you move assign. So a moved vector will have all the objects the target vector had and those needsto be `delete[]`ed. – Goswin von Brederlow Jun 10 '22 at 10:35
  • @GoswinvonBrederlow If the element/value type of std::vector is std::vector, when the outer vector reallocates it move constructs each std::vector in the new memory, "new (&newMemory[i]) std::vector(std::move(oldMemory[i])); " The old std::vector elements will have been moved from, meaning their pointers are null, and capacity is 0. Why do they have to be destroyed? – Zebrafish Jun 10 '22 at 10:45
  • It's not guaranteed that an empty vector has a nullptr for `data()`: *If size() is 0, data() may or may not return a null pointer.* So you may or may not have to call the destructors for vector>. And that is certainly true for other custom classes. – Goswin von Brederlow Jun 10 '22 at 10:58

1 Answers1

1

To end an object's lifetime, its destructor must be called. Also, what happens when an object it moved is up to the implementation of the class. You have two objects and the move constructor is allowed to move resources around how it sees fit.

An example of a simple string class which, when moving, leaves the moved-from object in a valid state:

class string {
public:
    string(const char* s) : m_len(std::strlen(s)), m_data(new char[m_len + 1]) {
        std::copy_n(s, m_len + 1, m_data);
    }
    string(string&& rhs) :
        m_len(std::exchange(rhs.m_len, 0)),
        m_data(std::exchange(rhs.m_data, new char[1]{})) 
    {}
    ~string() {
        delete[] m_data;
    }

private:
    size_t m_len;
    char* m_data;
};

Without calling the destructor of the moved-from object, it would leak. An implementation like above is not uncommon. Just because an object has been move-from, it doesn't mean that it's void of resources to free.

Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
  • 1
    std::string class dynamically allocates when constructing an empty string? Wow, that's terribly inefficient. I like the idea of passing an empty string to arguments to mean "nothing/null". This answers my question though, it turns out moving an object wasn't just what I though it was. – Zebrafish Jun 10 '22 at 10:52
  • @Zebrafish I just used `string` as an example. I don't think `std::string` looks exactly like this :-) - but it could, which is the point. – Ted Lyngmo Jun 10 '22 at 10:53
  • I've heard of small buffer optimisation, is that something like if (m_len > 4) ptr = new char[m_len]; else ptr = &sbo[0]; and then copy the string to the little array? – Zebrafish Jun 10 '22 at 11:01
  • @Zebrafish Yes, short string optimization is in all library implementation's I know of. It's basically a union/variant. If the string is short enough to fit within the `string` object, it's kept there but if it requires more space it'll dynamically allocate that memory. My silly-string in the example isn't that fancy :-) – Ted Lyngmo Jun 10 '22 at 11:07