Why isn't the observer_ptr
zeroed after a move operation?
It is correctly set to nullptr
in its default construction, and that does make sense (and prevents pointing to garbage).
And, accordingly, it should be zeroed when std::move()
'd from, just like std::string
, std::vector
, etc.
This would make it a good candidate in several contexts where raw pointers make sense, as well as automatic generation of move operations on classes with raw pointer data members, like in this case.
EDIT
As @JonathanWakely pointed out in the comments (and that is related to the aforementioned question):
if
observer_ptr
was null after a move it can be used to implement the Rule of Zero for types that have a pointer member. It's a very useful feature.
It seems like many people miss the point and the utility of this idea at first.
Consider:
template<typename Mutex>
class unique_lock
{
Mutex* pm;
public:
unique_lock() : pm() { }
unique_lock(Mutex& m) : pm(&m) { }
~unique_lock() { if (pm) pm->unlock(); }
unique_lock(unique_lock&& ul) : pm(ul.pm) { ul.pm = nullptr; }
unique_lock& operator=(unique_lock&& ul)
{
unique_lock(std::move(ul)).swap(*this);
return *this;
}
void swap(unique_lock& ul) { std::swap(pm, ul.pm); }
};
With a "dumb" smart pointer that is null-on-default-construction and null-after-move you can default three of the special member functions, so it becomes:
template<typename Mutex>
class unique_lock
{
tidy_ptr<Mutex> pm;
public:
unique_lock() = default; // 1
unique_lock(Mutex& m) : pm(&m) { }
~unique_lock() { if (pm) pm->unlock(); }
unique_lock(unique_lock&& ul) = default; // 2
unique_lock& operator=(unique_lock&& ul) = default; // 3
void swap(unique_lock& ul) { std::swap(pm, ul.pm); }
};
That's why it's useful to have a dumb, non-owning smart pointer that is null-after-move, like tidy_ptr
But observer_ptr
is only null-on-default-construction, so if it is standardized it will be useful for declaring a function to take a non-owning pointer, but it won't be useful for classes like the one above, so I'll still need another non-owning dumb pointer type. Having two non-owning dumb smart pointer types seems almost worse than having none!
So, move constructors are designed to make copy constructors cheaper in certain cases.
Let's write out what we'd expect these constructors and destructors to be: (This is a bit of a simplification, but that's fine for this example).
observer_ptr() {
this->ptr == nullptr;
}
observer_ptr(T *obj) {
this->ptr = obj;
}
observer_ptr(observer_ptr<T> const & obj) {
this->ptr = obj.ptr;
}
~observer_ptr() {
}
You're suggesting that the class provides a move constructor that looks like:
observer_ptr(observer_ptr<T> && obj) {
this->ptr = obj.ptr;
obj.ptr = null;
}
When I would suggest that the existing copy constructor will work fine as is, and is cheaper than the suggested move constructor.
std::vector
though?A std::vector, when copied, actually copies the array that it backs. So, a std::vector copy constructor looks something like:
vector(vector<T> const & obj) {
for (auto const & elem : obj)
this->push_back(elem);
}
The move constructor for the std::vector can optimize this. It can do this because that memory can be stolen from obj
. In a std::vector, this is actually useful to do.
vector(vector<T> && obj) {
this->data_ptr = obj.data_ptr;
obj.data_ptr = nullptr;
obj.size = 0;
}
Aside from the minor performance impact of zeroing the moved-from observer_ptr
, which is easily worked around (copy instead of move), the main rationale was probably to mimic the behaviour of regular pointers as closely as possible, following the principle of least surprise.
However, there is a potentially far more significant performance issue: defaulting the move functions allows an observer_ptr
to be trivially copyable. Trivial copyability allows an object to be copied using std::memcpy
. If observer_ptr
were not trivially copyable, neither would any classes with an observer_ptr
data member, resulting in a performance penalty that cascades down the compositional class hierarchy.
I have no idea what kind of performance improvements can be gained by using the std::memcpy
optimization, but they're probably more significant than the aforementioned minor performance issue.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With