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
asked Jun 22, 2016 at 0:31
\$\endgroup\$
1
  • \$\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\$ Commented Jun 23, 2016 at 21:54

1 Answer 1

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?

answered Jun 22, 2016 at 1:44
\$\endgroup\$
9
  • \$\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\$ Commented Jun 22, 2016 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\$ Commented Jun 22, 2016 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\$ Commented Jun 22, 2016 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\$ Commented Jun 22, 2016 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\$ Commented Jun 22, 2016 at 22:29

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.