A standalone guarded pointer pattern
Sometimes you have to deal with pointers of instances hold outside your responsibility. This can be due to using closed libraries or maintaining software which isn’t well designed but you cannot change it however. I recently got a case where a vector holds class instances directly inside itself (not as shared or unique pointer). This vector wasn’t accessible and the holding class did deliver only pointers to items of this vector:
class Item {};
class Holder {hugo
private:
std::vector<A> vec_;
public:
Item* getItem(void) noexcept {
return vec_.empty() ? nullptr : vec_.front();
}
};
Item *pi{};
{
Holder holder; // lets assume that Holder::vec_ is filled with Item instances
pi = holder.getItem();
}
// here holder is destroyed and so all Item instances within
if (pi) {
// problem: pi stills points to something, even if its not valid
}
Lets assume that the storage of Item
instances inside the vector isn’t allowed to be changed and the return value
from Holder::getItem
should still behave like a pointer.
What we need here is some kind of std::weak_ptr without the need of a shared pointer.
To check the validity of such weak pointer, a validator is required. It will be asked by each WeakPointer
having the same target
if before the target is accessed.
This validator should know if its target is invalid as soon as the target gets destroyed.
This leads to the next requirement: the target has to tell the validator when its gone.
This could be solved by a guard which becomes a member of the target class (T
in the following).
As seen the Target
owns a Guard
member which has a shared pointer to Validator
.
The Validator
holds a pointer to Target
which is both the target pointer as well as the validity check, more on this later.
The WeakPointer
also has a shared pointer to Validator
.
Most important: the Guard
is creator of Validator
, not the WeakPointer
!
The Validator
gets created with a pointer to Target
by the Guard
.
As soon as Target
gets destroyed, Guard
gets destroyed too and must set the pointer to Target
in Validator
to nullptr
.
The WeakPointer
gets the pointer to Target
only from the Validator
.
This way the pointer is valid as long as Target
exists.
As soon as Target
gets destroyed the WeakPointer
also points to nullptr
due to refering to it using the Validator
.
A long description, lets look at the implementation.
In the interests of simplicity, I have refrained from typing Guard
and Validator
and use a void*
pointer instead in Validator
(which is still save at this point due to the used encapsulation):
Validator
class Validator {
private:
// the pointer to the Target instance
void* object_ = nullptr;
// private, because only the Guard is allowed to set (construct) or invalidate the pointer to Target
void invalidate() noexcept {
object_ = nullptr;
}
Validator(void* instance)
:object_(instance)
{
if (!object_) {
// it doesn't make sense to watch for null objects
throw std::bad_weak_ptr();
}
}
public:
// Validator cannot be moved nor copied:
Validator(Validator&&) = delete;
Validator& operator=(Validator&&) = delete;
Validator(const Validator&) = delete;
Validator& operator=(const Validator&) = delete;
// The Guard is our friend, so it can access the constructor and invalidate() method directly.
friend class Guard;
operator bool() const noexcept { return object_ != nullptr; }
[[nodiscard]] inline void* ptr() noexcept { return object_; }
};
By setting both constructor and invalidation method to private, only the friend Guard
is allowed to access them.
Adding the bool
operator allows checking a Validator
instance directly for validity.
Guard
The Guard
is pretty small and simply owns the std::shared_pointer
to its Validator
, which is created directly on construction.
Note that I don’t use std::make_shared
for the Validator
creation. This would lead to making the Validator
constructor
public and so allowing accidential misusage - only the Guard
should be allowed creating the Validator
instance.
class Guard {
private:
std::shared_ptr<Validator> validator_;
public:
// Validator cannot be moved nor copied:
Guard(Guard&&) = delete;
Guard& operator=(Guard&&) = delete;
Guard(const Guard&) = delete;
Guard& operator=(const Guard&) = delete;
Guard(void* instance)
:validator_(new Validator(instance))
{}
~Guard() {
validator_->invalidate();
}
[[nodiscard]] inline std::shared_ptr<Validator> get_validator() const {
return { validator_ };
}
};
Why isn’t the Guard
allowed beeing copyied or moved?
Because the Guard
owns the Validator
and is responsible for setting the pointer to the instance correctly.
Copying or moving the Guard
would also copy or move the pointer to Target
implicitly, which is not what we want to do here.
Assume we would allow the Guard
beeing copyable:
MyClass inst;
WeakPointer<MyClass> wp{&inst}; // here the Guard of inst points to inst
MyClass inst_copy = inst; // copying Guard means that inst_copy`s Guard now also points to inst - not to inst_copy!
WeakPointer
Here is the most work to do, since instances of this class should behave like a regular pointer.
template < typename T >
class WeakPointer {
private:
std::shared_ptr<Validator> validator_;
[[nodiscard]] T* ptr() const noexcept {
return reinterpret_cast<T*>(validator_ ? validator_->ptr() : nullptr);
}
public:
// this allows any kind of WeakPointer class beeing a friend - no matter what its Target T is.
template <typename U> friend class WeakPointer;
// allow the right side equality and less than comparison operator beeing defined outside
template < typename T > friend bool operator==(const T*, const WeakPointer<T>&) noexcept;
template < typename T > friend bool operator<(const T*, const WeakPointer<T>&) noexcept;
// allow getting the container pointer
template < typename T > friend T* weakpointer_cast(WeakPointer<T>&) noexcept;
// construct directly with a validator - normally used by `T` instances delivering a weak pointer to themselves
WeakPointer(std::shared_ptr<Validator> validator)
:validator_(validator)
{}
// construct with a pointer to a `Target` instance.
// This requires that `T` defines a get_validator() method when used!
WeakPointer(T* instance) {
if (instance) {
validator_ = instance->get_validator();
}
}
// this copy constructor allows copying from a WeakPointer containing Target U, where U is either same as T or
// or derived from it.
template < typename U, typename = typename std::enable_if< std::is_base_of_v <T, U> || std::is_same_v<T, U> >::type >
WeakPointer(const WeakPointer<U>& other) {
validator_ = other.validator_;
}
// see copy constructor.
template < typename U, typename = typename std::enable_if< std::is_base_of_v <T, U> || std::is_same_v<T, U> >::type >
WeakPointer<T>& operator=(WeakPointer<U> other) {
validator_ = other.validator_;
return *this;
}
// check for equality with any other WeakPointer by comparing the Target it points to
template< typename U>
inline bool operator==(const WeakPointer<U>& other) const noexcept {
return ptr() == other.ptr();
}
// check if less than any other WeakPointer by comparing the Target it points to
template< typename U>
inline bool operator<(const WeakPointer<U>& other) const noexcept {
return ptr() < other.ptr();
}
// since the weak pointer could have a shared pointer to null for the `Validator`, here
// both the shared pointer to the `Validator` as well as the `Validator` itself (if any) are used for evaluation.
inline operator bool() const noexcept {
return validator_ && *validator_;
}
inline T* operator->() {
T* _p = ptr();
if (!_p) {
throw std::bad_weak_ptr();
}
return _p;
}
[[nodiscard]] inline T& operator*() {
T* _p = ptr();
if (!_p) {
throw std::bad_weak_ptr();
}
return *_p;
}
std::shared_ptr<Validator> get_validator() { return validator_; }
};
// for allowing dynamic casts without extracting the content of the WeakPointer we add this cast function.
// It performs a dynamic cast of the content of WeakPointer<U> to WeakPointer<T>, where U should be a base class of T
// (but not necessarily has to be).
template < typename T, typename U >
[[nodiscard]] WeakPointer<T> dynamic_weakpointer_cast(WeakPointer<U>& src) {
return { src ? dynamic_cast<T*>(reinterpret_cast<U*>(src.ptr())) : nullptr };
}
// allow equality comparison where Target is left and WeakPointer right:
template < typename T >
bool operator==(const T* elem, const WeakPointer<T>& wp) noexcept {
return elem == wp.ptr();
}
template < typename T >
bool operator<(const T* elem, const WeakPointer<T>& wp) noexcept {
return elem < wp.ptr();
}
// get the Target pointer from weak pointer
template < typename T >
[[nodiscard]] T* weakpointer_cast(WeakPointer<T>& src) noexcept {
return src.ptr();
}
The WeakPointer
defines the common used operator as bool operator, arrow operator (T* operator->
), the star operator (T& operator*
)
equality and less than operator which makes it useable like a normal pointer:
MyClass inst;
WeakPointer<MyClass> wp(&inst);
if (wp) {
WeakPointer<MyClass> wo = wp;
wp->foo();
(*wo).bar();
assert(wo == wp);
assert(!(wo < wp));
}
Additionally I added two global operators for equality and less than comparison between Target at left and a WeakPointer at right:
MyClass inst;
WeakPointer<MyClass> wp(&inst);
assert(wp == &inst); // --> WeakPointer<T>::operator==(const T*)
assert(&inst == wp); // --> operator==(const T*, WeakPointer<T>&)
assert(!wp < &inst); // --> WeakPointer<T>::operator<(const T*)
assert(!&inst < wp); // --> operator<(const T*, WeakPointer<T>&)
Target
The target class can be any type of class.
The only condition it has to fulfill is having a (optimally private) Guard
member which has to be constructed with this
and of
course a method returning a WeakPointer
to it:
class MyClass {
private:
Guard guard_;
public:
MyClass():guard_(this) {}
[[nodiscard]] WeakPointer<MyClass> get_weakpointer() {
return {guard_};
}
};
MyClass inst;
WeakPointer<MyClass> wp = inst.get_weakpointer();
assert(wp);
Optionally WeakPointer
s can be constructed directly with instances of the Target
class,
in such case a method get_validator
is required:
class MyClass {
private:
Guard guard_;
public:
MyClass():guard_(this) {}
[[nodiscard]] std::shared_ptr<Validator> get_validator() { return guard_->get_validator(); }
};
MyClass inst;
WeakPointer<MyClass> wp(&inst);
assert(wp);
Of course the target class can define both methods, but doesn’t have to depending on how it is used.
Important Since neither Guard
isn’t move- nor copyable, each class having a Guard
member has to explicitly init the Guard
member pointing to this
:
class MyClass {
private:
Guard guard_; // this is not allowed beeing copied to ensure always pointing to its owning instance!
std::string member; // this can be copied easily
public:
MyClass():guard_(this) {}
[[nodiscard]] std::shared_ptr<Validator> get_validator() { return guard_->get_validator(); }
MyClass(const MyClass& other)
:guard_(this)
,member(other.member)
{}
};
Usage
Now lets put it all together and do some examples.
# include <string>
using std::operator""s;
// base class having the Guard and both accessor methods.
class A {
public:
std::string name = "a"s;
A(void)
:guard_(this) // this part is important!
{}
virtual ~A() {}
[[nodiscard]] WeakPointer<A> get_weakpointer(void) {
return { get_validator() };
}
[[nodiscard]] std::shared_ptr<Validator> get_validator(void) noexcept {
return guard_.get_validator();
}
private:
Guard guard_;
};
// a simple class derived from A, no need to define additional members related to weak pointer
class B: public A {
public:
B(const std::string& n) {
name = n;
}
};
int main() {
// create empty (invalid) weak pointer
WeakPointer<B> pB;
WeakPointer<A> pA;
assert(!pB);
assert(!pA);
{
B b("b"s);
pA = b.get_weakpointer(); // this works because B is inherited from A
A* pa = &b;
pB = dynamic_weakpointer_cast<B>(pA); // this works because `a` points to an instance of B
assert(pB == pA);
assert(pA->name == pB->name);
assert((*pA).name == pB->name);
assert(pB);
assert(pA);
}
// scope of B is left, all weak pointers to it have to be invalid now.
assert(!pB);
assert(!pA);
return 0;
}
Not always having the possibility to change something requires creativity.
This is not the best solution in performance, due to having the Validator
as proxy to the Target
, but it increases safety.