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
-
\$\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– Chris Beck2016年06月23日 21:54:00 +00:00Commented Jun 23, 2016 at 21:54
1 Answer 1
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?
-
\$\begingroup\$ Thanks. Yes, you got the design I think. The main
unique_ref
is similar to aT &
, if it is left dangling then you get UB. But as long as that ref is properly managed, all theweak_ref
are okay. The idea is that there should be only one, or, few,unique_ref
. \$\endgroup\$Chris Beck– Chris Beck2016年06月22日 02:21:11 +00:00Commented 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\$Jan Korous– Jan Korous2016年06月22日 07:39:16 +00:00Commented 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 astd::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– Chris Beck2016年06月22日 20:18:59 +00:00Commented 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\$Chris Beck– Chris Beck2016年06月22日 22:28:51 +00:00Commented 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\$Jan Korous– Jan Korous2016年06月22日 22:29:11 +00:00Commented Jun 22, 2016 at 22:29