In C++ move constructor is required to reset the moved object which to me seems to be a duplication of what the destructor does in most of the cases.
Is it correct that defining a reset-method and using it in both destructor and move-constructor is the best approach? Or maybe there are better ways?
Move constructors typically "steal" the resources held by the argument (e.g. pointers to dynamically-allocated objects, file descriptors, TCP sockets, I/O streams, running threads, etc.) rather than make copies of them, and leave the argument in some valid but otherwise indeterminate state. For some types, such as std::unique_ptr
, the moved-from state is fully specified.
"Stolen" resources should not be released as this will usually lead to errors. For example, a move constructor which "steals" a pointer has to ensure that the destructor of the moved-from object won't delete
the pointer. Otherwise, there will be a double-free. A common way of implementing this is to reset the moved-from pointer to nullptr
.
Here is an example:
struct Pointer {
int *ptr;
// obtain a ptr resource which we will manage
Pointer(int* ptr) : ptr{ptr} {}
// steal another object's ptr resource, assign it to nullptr
Pointer(Pointer &&moveOf) : ptr{moveOf.ptr} {
moveOf.ptr = nullptr;
}
// make sure that we don't delete a stolen ptr
~Pointer() {
if (ptr != nullptr) {
delete ptr;
}
}
};
Is it correct that defining a reset-method and using it in both destructor and move-constructor is the best approach? Or maybe there are better ways?
This depends on the resource which is managed, but typically the destructor and move-constructor do different things. The move constructor steals the resource, the destructor frees a resource if it hasn't been stolen.
In C++ move constructor is required to reset the moved object which to me seems to be a duplication of what the destructor does in most of the cases.
You are right that there often is duplication of work. This is because C++ does not have destructive move semantics, so the destructor still gets called separately, even when an object has been moved from. In the example I have shown, ~Pointer()
still needs to get called, even after a move. This comes with the runtime cost of checking whether ptr == nullptr
. An example of a language with destructive move semantics would be Rust.
Related Posts: