Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why isn't observer_ptr zeroed after a move?

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.

like image 997
Mr.C64 Avatar asked Mar 10 '14 20:03

Mr.C64


3 Answers

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!

like image 181
Jonathan Wakely Avatar answered Nov 04 '22 20:11

Jonathan Wakely


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.

What about 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;
}
like image 35
Bill Lynch Avatar answered Nov 04 '22 20:11

Bill Lynch


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.

like image 1
Joseph Thomson Avatar answered Nov 04 '22 20:11

Joseph Thomson