如何使用移动语义正确管理资源?

How to correctly manage resources with move semantics?

struct foo{
  int* i;
  foo(): i(new int(42)){}
  foo(const foo&) = delete;
  foo(foo&&) = default;
  ~foo(){
    std::cout << "destructor: i" << std::endl;
    delete(i);
  }
};
int main()
{
  foo f;
  auto sp_f = std::make_shared<foo>(std::move(f));
}

这很糟糕,因为 f 的析构函数似乎一旦进入 shared_ptr 就会被调用。 shared_ptr会有一个被删除的指针,超出作用域后会被删除,也就是说这个指针会被删除两次。

如何避免这个问题?

您需要定义移动构造函数以防止从移出对象中删除:

foo(foo&& f): i(f.i) {
  f.i = nullptr;
}

现在当旧对象的析构函数是 运行 时,它不会删除 i,因为删除空指针是一个空指针。

您还应该定义移动赋值运算符并删除复制赋值运算符。

现在rule of three真的是五人制。如果你有一个可以移动的class,你应该自己定义移动语义(加上复制、析构函数等)。

至于如何做到这一点,引用 cppreference's page on std::move“...已移动的对象处于有效但未指定的状态。”未指定状态通常是对象在默认初始化时的样子,或者如果对象 swap 调用它们会发生什么。

正如@zenith 回答的那样,一种直接的方法是让移动构造函数(或赋值运算符)将原始指针设置为 nullptr。这样数据就不会被释放,原始对象仍然处于有效状态。

如前所述,另一个常见的习语是使用swap。如果 class 需要自己的复制和移动语义,那么 swap 方法也很方便。移动构造函数会将初始化委托给默认构造函数,然后调用 swap 方法。在移动赋值运算符中,只需调用 swap。被移入的对象将获得资源,另一个对象的析构函数将释放原来的资源。

通常看起来像这样:

struct Foo
{
    void* resource; //managed resource
    Foo() : resource(nullptr) {} //default construct with NULL resource
    Foo(Foo&& rhs) : Foo() //set to default value initially
    {
        this->swap(rhs); //now this has ownership, rhs has NULL
    }
    ~Foo()
    {
        delete resource;
    }
    Foo& operator= (Foo&& rhs)
    {
        this->swap(rhs); //this has ownership, rhs has previous resource
    }
    void swap(Foo& rhs) //basic swap operation
    {
        std::swap(resource, rhs.resource); //thanks @M.M
    }
};