I made an implementation of std::experimental::observer_ptr (library fundamentals TS v2). observer_ptr
basically behaves like a normal pointer and does no management of its target whatsoever (unlike std::unique_ptr
or std::shared_ptr
). It's usage is solely to indicate that it takes no ownership of the target object (it only observes it).
The implementation is simple and straight forward.
#include <cstddef>
#include <type_traits>
#include <memory>
namespace tb
{
template <typename T>
class observer_ptr
{
public:
using element_type = T;
constexpr observer_ptr() noexcept = default;
constexpr observer_ptr(std::nullptr_t) noexcept { }
template <typename U, typename = std::enable_if<!std::is_same_v<element_type, U> && std::is_convertible_v<U*, element_type*>>>
observer_ptr(observer_ptr<U> const& other) :
observer_ptr(static_cast<element_type*>(other.get())) {}
explicit observer_ptr(element_type* ptr) :
_data(ptr) { }
constexpr element_type* release() noexcept
{
auto* ptr = _data;
_data = nullptr;
return ptr;
}
constexpr void reset(element_type* p = nullptr) noexcept { _data = p; }
constexpr void swap(observer_ptr& other) noexcept
{
using std::swap;
swap(_data, other._data);
}
constexpr friend void swap(observer_ptr& lhs, observer_ptr& rhs) noexcept { lhs.swap(rhs); }
[[nodiscard]] constexpr element_type* get() const noexcept { return _data; }
[[nodiscard]] constexpr std::add_lvalue_reference_t<element_type> operator*() const { return *get(); }
[[nodiscard]] constexpr element_type* operator->() const noexcept { return get(); }
[[nodiscard]] constexpr explicit operator bool() const noexcept { return _data == nullptr; }
[[nodiscard]] constexpr explicit operator element_type*() const noexcept { return get(); }
private:
element_type* _data = nullptr;
};
template <typename T>
[[nodiscard]] observer_ptr<T> make_observer(T* ptr) noexcept
{
return observer_ptr<T>(ptr);
}
template <typename T1, typename T2>
[[nodiscard]] bool operator==(observer_ptr<T1> const& p1, observer_ptr<T2> const& p2)
{
return p1.get() == p2.get();
}
template <typename T1, typename T2>
[[nodiscard]] bool operator!=(observer_ptr<T1> const& p1, observer_ptr<T2> const& p2)
{
return !(p1 == p2);
}
template <typename T>
[[nodiscard]] bool operator==(observer_ptr<T> const& p, std::nullptr_t) noexcept
{
return static_cast<bool>(p);
}
template <typename T>
[[nodiscard]] bool operator==(std::nullptr_t, observer_ptr<T> const& p) noexcept
{
return static_cast<bool>(p);
}
template <typename T>
[[nodiscard]] bool operator!=(observer_ptr<T> const& p, std::nullptr_t) noexcept
{
return !p;
}
template <typename T>
[[nodiscard]] bool operator!=(std::nullptr_t, observer_ptr<T> const& p) noexcept
{
return !p;
}
template <typename T1, typename T2>
[[nodiscard]] bool operator<(observer_ptr<T1> const& p1, observer_ptr<T2> const& p2)
{
return p1.get() < p2.get();
}
template <typename T1, typename T2>
[[nodiscard]] bool operator>(observer_ptr<T1> const& p1, observer_ptr<T2> const& p2)
{
return p2 < p1;
}
template <typename T1, typename T2>
[[nodiscard]] bool operator<=(observer_ptr<T1> const& p1, observer_ptr<T2> const& p2)
{
return !(p2 < p1);
}
template <typename T1, typename T2>
[[nodiscard]] bool operator>=(observer_ptr<T1> const& p1, observer_ptr<T2> const& p2)
{
return !(p1 < p2);
}
}
Edit
Here is a sample usage
#include <vector>
#include <iostream>
struct A
{
void hello() const { std::cout << "hello\n"; }
};
// indicates that foo won't take ownership
void foo(tb::observer_ptr<A> const& ptr) { ptr->hello(); }
// does foo take ownership of ptr (especially if it were a member function)?
void foo(A* ptr) { ptr->hello(); }
int main()
{
std::vector<std::unique_ptr<A>> manager;
manager.emplace_back(new A);
manager.emplace_back(new A);
manager.emplace_back(new A);
for (auto& a : manager)
{
foo(tb::observer_ptr(a.get()));
foo(a.get());
}
return 0;
}
Some question that came to my mind:
- Should I add constructor overloads for
std::unique_ptr
andstd::shared_ptr
(for convenience)? - I'm usually not a fan of implicit conversions but in this case I'm thinking about making the constructors implicit. This is justified by (a)
observer_ptr
is a fairly light weight type and (b) it doesn't change the behavior or affect the underlying pointer in any way. What do you think about that?