正如我在Move constructor/operator=中所问的那样,过了一段时间我已经同意并接受了这个问题的正确答案,我只是在想,如果有类似“移动析构函数”之类的东西会在移动时被调用是否有用每次我们使用 move ctor 或 operator= 时的对象。
通过这种方式,我们只需要在 move dtor 中指定我们想要的内容,以及我们的对象在被移动构造函数使用后如何被取消。如果没有这种语义,看起来每次我写 move ctor 或 operator= 我都必须在它们中明确说明(代码重复/错误介绍)如何使移动的对象无效,这不是我认为的最佳选择。期待您对这个问题的意见。
3 回答
您能否举一个具体的例子,它会在哪里有用。例如,据我了解,移动分配在一般情况下可能被实现为
this->swap(rhv);
如果该类受益于移动语义,则 swap 方法在任何情况下都可能是有益的。这很好地将释放旧资源的工作委托*this
给常规析构函数。
如果没有显示一种新的析构函数是一种获得正确代码的优雅方式的特定示例,您的建议看起来就不是很吸引人。
此外,根据最新版本,移动构造函数/赋值运算符可以默认。这意味着我的课程很可能看起来像这样:
class X
{
well_behaved_raii_objects;
public:
X(X&& ) = default;
X& operator=(X&&) = default;
};
根本没有析构函数!是什么让我觉得有两个析构函数会吸引我?
还要考虑到赋值运算符有旧资源要处理。根据当前标准,您必须注意在构造和赋值之后正常的析构函数调用都很好,并且 IMO,与建议的移动析构函数类似,您必须在构造函数和赋值运算符中注意相同的移动析构函数可以是安全地调用。或者你想要两个移动析构函数 - 每个一个?:)
带有移动构造函数/赋值的注释中的msdn 示例的重新设计示例
#include <algorithm>
class MemoryBlock
{
public:
// Simple constructor that initializes the resource.
explicit MemoryBlock(size_t length)
: length(length)
, data(new int[length])
{
}
// Destructor.
~MemoryBlock()
{
delete[] data; //checking for NULL is NOT necessary
}
// Copy constructor.
MemoryBlock(const MemoryBlock& other)
: length(other.length)
, data(new int[other.length])
{
std::copy(other.data, other.data + length, data);
}
// Copy assignment operator (replaced with copy and swap idiom)
MemoryBlock& operator=(MemoryBlock other) //1. copy resource
{
swap(other); //2. swap internals with the copy
return *this; //3. the copy's destructor releases our old resources
}
//Move constructor
//NB! C++0x also allows delegating constructors
//alternative implementation:
//delegate initialization to default constructor (if we had one), then swap with argument
MemoryBlock(MemoryBlock&& other)
: length(other.length)
, data(other.data)
{
other.data = 0; //now other can be safely destroyed
other.length = 0; //not really necessary, but let's be nice
}
MemoryBlock& operator=(MemoryBlock&& rhv)
{
swap(rhv);
//rhv now contains previous contents of *this, but we don't care:
//move assignment is supposed to "ruin" the right hand value anyway
//it doesn't matter how it is "ruined", as long as it is in a valid state
//not sure if self-assignment can happen here: if it turns out to be possible
//a check might be necessary, or a different idiom (move-and-swap?!)
return *this;
}
// Retrieves the length of the data resource.
size_t Length() const
{
return length;
}
//added swap method (used for assignment, but recommended for such classes anyway)
void swap(MemoryBlock& other) throw () //swapping a pointer and an int doesn't fail
{
std::swap(data, other.data);
std::swap(length, other.length);
}
private:
size_t length; // The length of the resource.
int* data; // The resource.
};
对原始 MSDN 示例的一些评论:
1)之前检查 NULLdelete
是不必要的(也许这里是为了我已经剥离的输出完成的,也许这表明存在误解)
2)在赋值运算符中删除资源:代码重复。使用复制和交换习语,删除先前持有的资源被委托给析构函数。
3) copy-and-swap 习惯用法也使得自分配检查变得不必要。如果资源在删除之前被复制,这不是问题。- (另一方面,“无论如何复制资源”只会在您期望通过此类完成大量自我分配时受到伤害。)
4) MSDN 示例中的赋值运算符缺乏任何类型的异常安全性:如果分配新存储失败,则该类将处于无效状态并带有无效指针。销毁后将发生未定义的行为。
这可以通过仔细重新排序语句并在中间将已删除的指针设置为 NULL 来改进(不幸的是,这个特定类的不变性似乎是它总是拥有一个资源,所以让它干净地丢失资源以防例外也不完美)。相比之下,使用复制和交换,如果发生异常,左侧值将保持其原始状态(更好的是,操作无法完成,但可以避免数据丢失)。
5) 自赋值检查在移动赋值运算符中看起来特别有问题。首先,我看不出左值与右值如何相同。是否需要a = std::move(a);
实现身份(看起来无论如何都是未定义的行为?)?
6) 同样,移动分配是不必要地管理资源,我的版本只是将其委托给常规析构函数。
结论:您看到的代码重复是可以避免的,它只是由一个幼稚的实现引入(出于某种原因,您倾向于在教程中看到这种实现,可能是因为重复的代码对学习者来说更容易理解)。
为防止资源泄漏,请始终在移动赋值运算符中释放资源(例如内存、文件句柄和套接字)。
...如果您可以重复代码重复,否则重用析构函数。
为了防止资源的不可恢复的破坏,请在移动赋值运算符中正确处理自赋值。
...或者确保在确定可以替换之前从不删除任何内容。或者更确切地说是一个 SO 问题:在定义明确的程序中移动分配的情况下是否可能发生自分配。
此外,从我的草稿(3092)中,我发现如果一个类没有用户定义的复制构造函数/赋值运算符,并且没有任何东西可以阻止移动构造函数/赋值的存在,那么一个类将被隐式声明为 defaulted。如果我没记错的话,这意味着:如果成员是字符串、向量、shared_ptrs 等,在这种情况下,您通常不会编写复制构造函数/赋值,您将免费获得移动构造函数/移动赋值.
这有什么问题:
struct Object
{
Object(Object&&o) { ...move stuff...; nullify_object(o); }
Object & operator = (Object && o) { ...; nullify_object(o); }
void nullify_object(Object && o);
};
或者在目标上调用 nullify_object 的替代方法:o.nullify();
我看不到添加 YANLF 的主要好处。
移动构造函数/赋值是您窃取资源并将被盗对象资源的状态保留在一个状态,以便在调用其析构函数时能够安全销毁该对象。除了移动构造函数/赋值之外,您无法查看或访问从中窃取资源的临时“值”。
例如,让我们以一个字符串为例。在那里,您将从临时对象中窃取分配的资源和大小,并将其值设置为您自己的值(如果您的对象是默认构造的,则该值应为 null 和 0)。