移动的对象仍然被破坏?

Moved objects are still destructed?

在学习 C++11 时,我对移动对象的行为方式感到惊讶。考虑这段代码:

#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)};
}

它产生这个输出:

$ 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

我很惊讶,因为我希望创建一个可移动的 RAII 类型。然而,似乎每个移动的中间体都被破坏了!

是否有一些变体允许我在 "object's lifetime" 结束时释放我的资源一次? (也就是说,在移动对象序列的生命周期结束时?)

处于类似情况的人可能应该使用 std::unique_ptr 并完成。然而,在这种情况下,~Moveable() 可能会抛出异常,显然 std::unique_ptr 的析构函数将在出现异常时终止程序(至少在 clang 3.8.0 中是这样)

是的,移出的对象被销毁了。它们仍处于未确定但有效的状态。它们仍然是对象。

最好记得 C++ 实际上并没有移动任何东西。 std::move 只是给你一个右值。所谓的 "move constructors" 只是复制构造函数的方便替代品,当你有一个右值时在查找过程中找到它,并允许 有机会交换你的 class封装数据而不是实际复制它。但是 C++ 不会为你移动任何东西,它也不知道你什么时候做了一些移动。

因此,对于 C++ 来说,拥有任何一种以某种方式阻止 "moved-from" 对象的规则都是不切实际的危险和不切实际的,如果我们甚至可以确定这通常意味着什么,以后会被销毁。使这种销毁对您的移动对象安全(理想情况下是无操作)(例如,通过在移动构造函数中将源指针设置为 nullptr),您会没事的。

"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?)"

这是应该发生的事情。不要忘记,当 moved from 对象被销毁时,您的 move constructor 已经删除了它的资源,所以它没有任何东西可以释放。只有最后的 moved to 对象保留了要释放的资源。

稍微修改你的代码并引入几个跟踪变量,我们可以更清楚地看到发生了什么,我还通过添加我们 "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

如我们所料,原始对象最后被销毁。我们的 move 构造函数转移了它正在跟踪的资源,因此它最终被 #2 而不是 #0 跟踪——对象 #0 和 #1 是空的;如果我们使用 std::unique_ptr<> 或其他东西来拥有资源,则只有一个对象会尝试删除它。

请注意,是 移动构造函数 而不是 std::move 调用导致了此传输。

是的,移出的对象仍然被破坏。要在所有移动之后正确释放一次资源,我们需要告诉析构函数对象何时从以下位置移动:

#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)};
}

这会产生

$ 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

将此作为您资源的基础class:

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;
    }
};

然后您只需在析构函数中检查 isMine() 并在为真时解除分配/释放。这允许常量字段。

如果关闭并重新打开同一资源是一种有效的方案,请考虑使用 std::optional<MyResource> 和采用此类型的免费函数来进行对已关闭的流也有效的操作(例如,重新打开)。如果你不喜欢自由函数,你可以把它们放在静态助手中 class.