移动对象仍会被销毁吗?

10

在学习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类型。然而,似乎每个移动的中间件都被销毁了!有没有一种变化可以让我在“对象的生命周期”结束时释放资源?(也就是,在移动对象序列的生命周期结束时)在类似情况下,应该使用std::unique_ptr并完成。但是,在这种情况下,可能会出现〜Moveable()抛出异常,显然std::unique_ptr的析构函数将终止异常程序(至少在clang 3.8.0中如此)。

7
移动来源的对象仍会被析构。如果您操作得当,析构一个移动来源的对象不会释放已从中移出的资源。 - user2357112
1
是的。已移动对象仍会被销毁。std::unique_ptr 内部检查了空指针,如果为空则不会释放资源以避免此问题。 - milleniumbug
这可能会有所帮助:https://dev59.com/XJjga4cB1Zd3GeqPMZiu#38171996 - tkausl
5
请注意,析构函数抛出异常的做法值得认真重新考虑。从所有标准库容器开始,几乎所有人都默认析构函数不会抛出异常。请确保翻译内容通俗易懂且与原文意思相符。 - milleniumbug
5个回答

13

是的,被移动的对象会被销毁。它们仍然是对象,但状态无法确定。

记住,C++ 实际上并没有移动任何东西。 std::move 只是给你一个右值。所谓的“移动构造函数”只是复制构造函数的便利替代品,在查找时找到一个右值,并允许您交换类的封装数据而不是实际复制它。但 C++ 并没有为您移动任何东西,也无法判断您是否进行了某些移动。

因此,如果 C++ 有任何一种规则以阻止“被移动的”对象在以后被销毁,这将是不可行且危险的,即使我们能够在一般情况下决定这意味着什么。使这些被移动的对象的销毁安全(最好是无操作),例如通过在移动构造函数中设置源指针为nullptr,那么您将没问题。


你的回答中可操作的部分似乎是创建一种方式,使每个对象能够将自己标记为“已移动”,并在对象注意到自己已被移动时强制析构函数不执行任何操作。如果我理解正确的话,也许你可以在你的回答中详细说明一下? - phs
2
而且,将移动的对象重新设置为适当的值也是完全有效的。例如,std::unique_ptr<X> px = std::move(old); old = std::make_unique<X>(); - SergeyA

2

是的,被移动的对象仍然会被销毁。为了在所有移动之后正确释放资源,我们需要告诉析构函数对象已经被移动:

#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

0
“有没有一种变体可以让我在我的“对象生命周期”的末尾(也就是移动对象序列的生命周期结束时)释放资源?”
这就是应该发生的事情。不要忘记,当“移动自”对象被销毁时,您的“移动构造函数”已经删除了它的资源,因此它没有剩余的资源需要释放。只有最终的“移动到”对象留下来持有要释放的资源。”

0
通过稍微修改您的代码并引入一些跟踪变量,我们可以更清楚地看到发生了什么,并且我还演示了通过添加一个我们“移动”的资源来表明移动的意图:
#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)};
}

输出

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<>` 或类似的东西来拥有资源,那么只有一个对象会尝试删除它。
请注意,导致这种转移的是 `move constructor` 而不是 `std::move` 调用。

0
把这个视为你的资源的基类:
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(),如果为真,则释放/释放内存。这允许使用const字段。

如果关闭并重新打开相同的资源是有效的场景,请考虑使用std::optional<MyResource>和自由函数来执行对已关闭流也有效的操作(例如,重新打开)。如果您不喜欢自由函数,可以将它们放在静态辅助类中。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接