2
\$\begingroup\$

In a few of my projects, I had sort of a common situation, where I wanted to share a pointer to some object, and I want the recipient to be able to check if it is still valid, but I don't want them to "share ownership" of the object that is pointed to.

To make it concrete, suppose I have some service class that does something useful. And, I want to make a gui interface to it. That means, I'm going to use some existing gui framework, and pass delegates to it that call various members of this service object. I didn't write the gui framework, it has it's own management system and I don't necessarily know exactly when it's going to delete those delegates. I'm not thrilled about a bunch of dead pointers floating around inside of it.

Now you might say, oh, just use a std::shared_ptr or std::weak_ptr, and bind your delegates to that. However, my service class is not owned by a std::shared_ptr and I don't necessarily want to commit to that. I don't really want the GUI to be able to take ownership of the service, that seems wrong. I want to know exactly what the lifetime of the service is, and all of its resources.

The quick and dirty thing that I usually did was, if the service is of type T, then the service owns a std::shared_ptr<T*> which it initializes with this. Then std::weak_ptr are produced from that, and the delegates use that. Because expressions like if (auto l = ptr_.lock()) { something(**l); ... } are ugly, I put this behind some common interface, where .lock() returns a T* which is nullptr if we can no longer access the object.

Because I had a few classes like this in my projects, I decided to extract and make a common implementation. I also decided that, it should not be based on std::shared_ptr, since it has overhead for thread-safety and having multiple owners. If .lock() returns a T* then it can never be thread-safe, but these apps I worked on weren't passing these things between threads anyways.

I tried to make a familiar interface and decently optimize the result. I guess my plan now is to put the new version into my other projects, and also open source it and put it on my github. But I would appreciate some code review first :)

//  (C) Copyright 2015 - 2016 Christopher Beck

//  Distributed under the Boost Software License, Version 1.0. (See accompanying
//  file LICENSE or copy at http://www.boost.org/LICENSE_1_0.txt)

#ifndef NONSTD_WEAK_REF_HPP_INCLUDED
#define NONSTD_WEAK_REF_HPP_INCLUDED

/***
 * Synopsis:
 * unique_ref and weak_ref are a pair of "smart pointer-like" objects.
 *
 * - unique_ref is constructed from a reference to an object (or, default
     constructed.)
 * - weak_ref can be constructed from unique_ref.
 * - They do not manage the lifetime of the thing they are referring to,
     instead, the unique_ref merely manages the validity of the weak_ref's,
     making it easier to manage the possibility of dangling pointers. (But not
     100% eliminating it.)
 * - On the plus side, it has significantly less overhead, and can be used
     with any object, even a stack-allocated one.
 * - "weak_ref::lock" returns a raw pointer rather than a smart pointer, since
     it is not locking in the sense of taking ownership. It's merely returning
     a validity-checked pointer -- if it's not nullptr, then it is safe to
     dereference, unless the "unique_ref" itself is dangling (easier to manage.)
 * - They are not thread safe, they should not be passed across threads. If you
     need that then you should use `std::shared_ptr` instead.
 * - The interface mimics `std::weak_ptr`.
 * - There is no possibility of leaks due to "cyclic references". The only object
     whose lifetime is managed by reference counting here, is a shared control
     structure, whose destructor is trivial.
 */

namespace nonstd {

namespace detail {

template <typename T>
struct weak_ref_control_structure {
  T * payload_;
  mutable long ref_count_;

  explicit weak_ref_control_structure(T * t) : payload_(t), ref_count_(0) {}
};

} // end namespace detail

// Forward declare weak_ref
template <typename T>
class weak_ref;

// unique_ref: Owner of control structure
template <typename T>
class unique_ref {
  using ctrl_t = detail::weak_ref_control_structure<T>;

  ctrl_t * ptr_;

  void init(T & t) { ptr_ = new ctrl_t(&t); }

  void move(unique_ref & o) {
    ptr_ = o.ptr_;
    o.ptr_ = nullptr;
  }

  // Invariant: If ptr_ is not null, it points to a ctrl_t that no other
  // unique_rf points to, and ptr_->payload_ is also not null.

  friend class weak_ref<T>;

public:
  // Special member functions
  unique_ref() : ptr_(nullptr) {}
  unique_ref(T & t) { this->init(t); }

  ~unique_ref() { this->reset(); }

  unique_ref(unique_ref && other) { this->move(other); }

  unique_ref & operator = (unique_ref && other) {
    this->move(other);
    return *this;
  }

  // Copy ctor: Make a new ctrl structure pointing to the same payload
  unique_ref(const unique_ref & other) {
    if (other.ptr_) {
      this->init(*other.ptr_->payload_);
    } else {
      ptr_ = nullptr;
    }
  }

  // Copy assignment: Copy and swap
  unique_ref & operator = (const unique_ref & other) {
    unique_ref temp{other};
    this->swap(temp);
    return *this;
  }

  // Reset (release managed object)
  void reset() {
    if (ptr_) {
      ptr_->payload_ = nullptr;
      if (!ptr_->ref_count_) {
        delete ptr_;
      }
      ptr_ = nullptr;
    }
  }

  // Swap
  void swap(unique_ref & other) {
    ctrl_t * temp = ptr_;
    ptr_ = other.ptr_;
    other.ptr_ = temp;
  }

  // Observers
  // Get the managed pointer
  T * get() const {
    if (ptr_) { return ptr_->payload_; }
    return nullptr;
  }

  // Operator *: Blindly dereference the getted pointer, without a null check.
  T & operator *() const {
    return *this->get();
  }

  // Operator bool: check if there is a managed object
  explicit operator bool() const { return ptr_; }

  // use_count: mimic std::shared_ptr interface
  long use_count() const {
    return ptr_ ? 1 : 0;
  }

  // unique: mimic std::shared_ptr interface
  bool unique() const {
    return this->use_count() == 1;
  }

  // weak_ref_count: do something more useful :)
  long weak_ref_count() const {
    if (ptr_) { return ptr_->ref_count_; }
    return 0;
  }
};

template <typename T>
class weak_ref {
  using ctrl_t = detail::weak_ref_control_structure<T>;

  // Rationale: When we lock the weak_ref, if the ref has expired, we want to
  // release this immediately, and set to nullptr, so that future lookups are
  // faster. Since the caller is going to test the pointer we return anyways,
  // this should be a cheap operation in an optimized build.
  mutable const ctrl_t * ptr_;

  void init(const ctrl_t * c) {
    if (c) {
      ++(c->ref_count_);
    }
    ptr_ = c;
  }

  void move(weak_ref & o) {
    ptr_ = o.ptr_;
    o.ptr_ = nullptr;
  }

  void release() const {
    if (ptr_) {
      if (!--(ptr_->ref_count_)) {
        delete ptr_;
      }
      ptr_ = nullptr;
    }
  }

public:
  // Special member functions
  weak_ref() : ptr_(nullptr) {}
  weak_ref(const unique_ref<T> & u) {
    this->init(u.ptr_);
  }
  weak_ref(const weak_ref & o) {
    this->init(o.ptr_);
  }
  weak_ref(weak_ref && o) { this->move(o); }
  ~weak_ref() { this->release(); }

  weak_ref & operator = (const weak_ref & o) {
    this->release();
    this->init(o.ptr_);
    return *this;
  }

  weak_ref & operator = (weak_ref && o) {
    this->release();
    this->move(o);
    return *this;
  }

  // Swap
  void swap(weak_ref & o) {
    const ctrl_t * temp = ptr_;
    ptr_ = o.ptr_;
    o.ptr_ = temp;
  }

  // Reset is not const qualified, from user perspective this makes the most sense.
  void reset() {
    this->release();
  }

  // Lock: Obtain the payload if possible, otherwise return nullptr
  T * lock() const {
    if (ptr_) {
      T * result = ptr_->payload_;
      if (!result) { this->release(); }
      return result;
    }
    return nullptr;
  }

  // Expired: cast this->lock() to bool
  bool expired() const {
    return static_cast<bool>(this->lock());
  }

  // use_count: mimic std::shared_ptr interface
  long use_count() const {
    return this->expired() ? 1 : 0;
  }

  // weak_ref_count: do something more useful :)
  long weak_ref_count() const {
    if (ptr_) {
      if (ptr_->payload_) {
        return ptr_->ref_count_;
      }
      this->release();
    }
    return 0;
  }
};

} // end namespace nonstd

#endif // NONSTD_WEAK_REF_HPP_INCLUDED
\$\endgroup\$
  • \$\begingroup\$ Note: I put this on github here with some updates and improvements. Thanks to Jan Korous for comments :) github.com/cbeck88/weak_ref \$\endgroup\$ – Chris Beck Jun 23 '16 at 21:54
1
\$\begingroup\$

explicit constructors

I would definitely make all single parameter constructors explicit.

typo

// unique_rf points to, and ptr_->payload_ is also not null.

Should be probably

// unique_ref points to, and ptr_->payload_ is also not null.

interface

I am not sure I understand your design. Do I get it correct that whenever you call unique_ref(T & t) constructor and object t is referring to actually goes out of scope you find yourself dangerously close to undefined behavior with dangling reference in your hand?

\$\endgroup\$
  • \$\begingroup\$ Thanks. Yes, you got the design I think. The main unique_ref is similar to a T &, if it is left dangling then you get UB. But as long as that ref is properly managed, all the weak_ref are okay. The idea is that there should be only one, or, few, unique_ref. \$\endgroup\$ – Chris Beck Jun 22 '16 at 2:21
  • \$\begingroup\$ @ChrisBeck Wouldn't it possibly make sense to rethink the design so that unique_ref is actually able to manage the referred resource? I could imagine in-place construction with arguments forwarded, moving the resource or something like that. \$\endgroup\$ – Jan Korous Jun 22 '16 at 7:39
  • \$\begingroup\$ I mean then, you probably just want std::shared_ptr I guess. I agree that that strategy has a lot going for it. But it has some drawbacks -- once you have something in a std::shared_ptr you don't know exactly when it will be deleted (maybe the order relative to something else is important for some reason?). And you don't get as compact of a memory layout since you have to make a dynamic allocation. unique_ref is really only for the edge cases where those two things are very unattractive. \$\endgroup\$ – Chris Beck Jun 22 '16 at 20:18
  • \$\begingroup\$ I guess I am going to change the interface, so that unique_ref is constructed from a pointer and not a reference. That way, when you use it, you have to explicitly take a pointer and pass it away, to make it more explicit that badness could be occurring if you aren't careful. \$\endgroup\$ – Chris Beck Jun 22 '16 at 22:28
  • \$\begingroup\$ @ChrisBeck Not necessarily, I think there are other ways than shared_ptr. But I am not convinced about compact memory layout as it is dereferrencing all the way to referred object which is living who know where (possibly heap). \$\endgroup\$ – Jan Korous Jun 22 '16 at 22:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.