Moved objects are still destructed?
Asked Answered
A

5

10

While learning C++11, I was surprised by the way moved objects appear to behave. Consider this code:

#include <exception>
#include <iostream>
#include <type_traits>

class Moveable {
public:
  Moveable() {
    std::cout << "Acquire odd resource\n";
  }

  ~Moveable() noexcept(false) {
    std::cout << "Release odd resource\n";
    // if (!std::uncaught_exception() && error_during_release) {
    //   throw std::exception("error");
    // }
  }

  Moveable(Moveable const &) = delete;
  Moveable &operator=(Moveable const &) = delete;

  Moveable(Moveable &&) = default;
  Moveable &operator=(Moveable &&) = default;
};

int main(int argc, char *argv[]) {
  static_assert(!std::is_copy_constructible<Moveable>::value,
    "is not copy constructible");
  static_assert(!std::is_copy_assignable<Moveable>::value, "is not copy assignable");
  static_assert(std::is_move_constructible<Moveable>::value, "is move constructible");
  static_assert(std::is_move_assignable<Moveable>::value, "is move assignable");

  Moveable moveable{};
  Moveable moved{std::move(moveable)};
  Moveable moved_again{std::move(moved)};
}

It yields this output:

$ clang++ --version
clang version 3.8.0 (tags/RELEASE_380/final)
Target: x86_64-unknown-linux-gnu
Thread model: posix
InstalledDir: /opt/clang+llvm-3.8.0-x86_64-linux-gnu-ubuntu-14.04/bin
$ clang++ --std=c++14 --stdlib=libc++ -Wall -Werror -o move_and_destroy move_and_destroy.cc  && ./move_and_destroy
Acquire odd resource
Release odd resource
Release odd resource
Release odd resource

I'm surprised because I was hoping to create a movable RAII type. However it seems each moved intermediate is destructed!

Is there some variation of this that allows me to release my resource once at the end of my "object's lifetime"? (that is, at the end of the lifetimes of the sequence of moved objects?)

Someone in a similar situation should probably use std::unique_ptr and be done. However in this scenario it is possible for ~Moveable() to throw, and apparently std::unique_ptr's destructor will terminate the program on exception (at least, in clang 3.8.0.)

Adamo answered 10/7, 2016 at 0:10 Comment(4)
Moved-from objects still get destructed. If you're doing things right, destructing a moved-from object won't free the resources that were moved out of it.Shoran
Yes. Moved-from objects are still destroyed. std::unique_ptr internally checks for null pointer and it doesn't release the resource if it's null to avoid this problemHarrington
This might help: #38172464Mccreary
Also, seriously reconsider throwing from the destructor. Pretty much everyone assumes destructors don't throw, starting from the all standard library containers.Harrington
W
13

Yes, moved-from objects are destructed. They remain in an undetermined but valid state. They're still objects.

It's best if you recall that C++ doesn't actually move anything. std::move just gives you an rvalue. So-called "move constructors" are just convenient alternatives to copy constructors, found during lookup when you have an rvalue, and allowing you the opportunity to swap your class's encapsulated data rather than actually copying it. But C++ doesn't move anything for you, and it can't tell when you have done some moving.

As such, it would be infeasibly dangerous and impractical for C++ to have any kind of rule that somehow stopped "moved-from" objects, if we could even decide what this meant in general, from later undergoing destruction. Make this destruction safe (a no-op, ideally) for your moved-from objects (e.g. by setting source pointers to nullptr in your move constructor) and you'll be fine.

Wellthoughtof answered 10/7, 2016 at 0:16 Comment(2)
It seems the actionable part of your answer is to create a way for each object to mark itself as "moved", and force the destructor to no-op when an object notices it has been moved from. If I understand that correctly, maybe you could expand on it in your answer?Adamo
And to add, it is perfectly valid to set moved object to a proper value again. For example, std::uniqiue_ptr<X> px = std::move(old); old = std::make_unique<X>();Cromagnon
A
2

Yes, moved-from objects are still destructed. To correctly release the resource once, after all moves, we need to tell the destructor when the object has been moved from:

#include <exception>
#include <iostream>
#include <type_traits>

class Moveable {
private:
  bool moved_from;

public:
  Moveable() : moved_from(false) {
    std::cout << "Acquire odd resource\n";
  }

  ~Moveable() noexcept(false) {
    // We have already been moved from! Do nothing.
    if (moved_from) {
      std::cout << "Not releasing odd resource\n";
      return;
    }

    std::cout << "Release odd resource\n";
    // if (!std::uncaught_exception() && error_during_release) {
    //   throw std::exception("error");
    // }
  }

  Moveable(Moveable const &) = delete;
  Moveable &operator=(Moveable const &) = delete;

  Moveable(Moveable &&moveable) {
    moved_from = false;
    moveable.moved_from = true;
    // And now we spell out the explicit default move constructor
  }

  Moveable &operator=(Moveable &&moveable) {
    moved_from = false;
    moveable.moved_from = true;
    // And now we spell out the explicit default move assignment operator
    return *this;
  }
};

int main(int argc, char *argv[]) {
  static_assert(!std::is_copy_constructible<Moveable>::value,
    "is not copy constructible");
  static_assert(!std::is_copy_assignable<Moveable>::value, "is not copy assignable");
  static_assert(std::is_move_constructible<Moveable>::value, "is move constructible");
  static_assert(std::is_move_assignable<Moveable>::value, "is move assignable");

  Moveable moveable{};
  Moveable moved{std::move(moveable)};
  Moveable moved_again{std::move(moved)};
}

This yields

$ clang++ --std=c++14 --stdlib=libc++ -Wall -Werror -o move_and_destroy move_and_destroy.cc  && ./move_and_destroy
Acquire odd resource
Release odd resource
Not releasing odd resource
Not releasing odd resource
Adamo answered 10/7, 2016 at 0:36 Comment(0)
S
0

"Is there some variation of this that allows me to release my resource once at the end of my "object's lifetime"? (that is, at the end of the lifetimes of the sequence of moved objects?)"

This is what should happen. Don't forget that by the time the moved from object is destroyed your move constructor has removed its resources so it has nothing left to deallocate. Only the final moved to object is left holding the resources to be freed.

Seam answered 10/7, 2016 at 0:21 Comment(0)
C
0

Modifying your code slightly and introducing a couple of tracking variables, we can see what's happening more clearly, and I also demonstrate the intent of moving by adding a resource that we "move":

#include <exception>
#include <iostream>
#include <type_traits>

class Moveable {
  static int s_count;
  int m_id;
  const char* m_ptr;
public:
  Moveable(const char* ptr) : m_id(s_count++), m_ptr(ptr) {
    std::cout << "Moveable(ptr) " << m_id << '\n';
  }

  Moveable(Moveable&& rhs) : m_id(s_count++), m_ptr(nullptr) {
    std::cout << "Moveable(&&) " << m_id << " from " << rhs.m_id << '\n';
    std::swap(m_ptr, rhs.m_ptr);
  }

  ~Moveable() noexcept(false) {
    std::cout << "Release " << m_id << " m_ptr " << (void*)m_ptr << '\n';
  }

  Moveable(Moveable const &) = delete;
  Moveable &operator=(Moveable const &) = delete;
};

int Moveable::s_count;

int main(int argc, char *argv[]) {
  Moveable moveable{"hello world"};
  Moveable moved{std::move(moveable)};
  Moveable moved_again{std::move(moved)};
}

The output:

Moveable(ptr) 0
Moveable(&&) 1 from 0
Moveable(&&) 2 from 1
Release 2 m_ptr 0x8048a26
Release 1 m_ptr 0
Release 0 m_ptr 0

As we expect, the original object is destroyed last. Our move constructor, tho, transfered the resource it was tracking so that it ultimately wound up being tracked by #2 not #0 -- objects #0 and #1 are empties; if we were using a std::unique_ptr<> or something to own the resource, only one of the objects would have tried to delete it.

Note that it was the move constructor not the std::move invocation that caused this transfer.

Crossgrained answered 10/7, 2016 at 0:35 Comment(0)
P
0

Consider this as a base class for your resources:

class Resource
{
private:
     mutable bool m_mine;

protected:
    Resource()
    : m_mine( true )
    {
    }

    Resource(const Resource&)       = delete;
    void operator=(const Resource&) = delete;

    Resource(const Resource&& other)
    : m_mine( other.m_mine )
    {
        other.m_mine = false;
    }

    bool isMine() const
    {
        return m_mine;
    }
};

Then you just check isMine() in your destructor and deallocate / release if true. This allows for const fields.

If it's a valid scenario to close and reopen the same resource, consider using std::optional<MyResource> and free functions taking this type for operations that are valid on a closed stream as well (e.g., reopen). If you don't like free functions, you might put them in a static helper class.

Piecedyed answered 10/7, 2016 at 14:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.