What exactly is the point of the construct std::observer_ptr
in the library fundamentals technical specification V2?
It seems to me that all it does is wrap a bare T*
, which seems like a superfluous step if it adds no dynamic memory safety.
In all of my code I use std::unique_ptr
where I need to take explicit ownership of an object and std::shared_ptr
where I can share ownership of an object.
This works very well and prevents accidental dereferencing of an already destroyed object.
std::observer_ptr
makes no guarantee about the lifetime of the object observed, of course.
If it were to be constructed from a std::unique_ptr
or std::shared_ptr
I would see a use in such a structure, but any code that is simply using T*
is probably just going to keep doing so and if they plan on moving to anything it would be std::shared_ptr
and/or std::unique_ptr
(depending on use).
Given a simple example function:
template<typename T>
auto func(std::observer_ptr<T> ptr){}
Where it would be useful if it stopped smart pointers from destroying their stored object while they are being observed.
But if I want to observe a std::shared_ptr
or std::unique_ptr
I have to write:
auto main() -> int{
auto uptr = std::make_unique<int>(5);
auto sptr = std::make_shared<int>(6);
func(uptr.get());
func(sptr.get());
}
Which makes it no safer than:
template<typename T>
auto func(T *ptr){}
So, what is the use of this new structure?
Is it just for self-documenting source?
It seems from the proposal that
std::observer_ptr
is largely for documenting that a pointer is a non-owning reference to an object, rather than an owning reference, array, string or iterator.However there are a couple of other benefits to using
observer_ptr<T>
overT*
:observer_ptr
will always be initialized tonullptr
; a regular pointer may or may not be initialized, depending on the context.observer_ptr
only supports operations which make sense for a reference; this enforces correct usage:operator[]
is not implemented forobserver_ptr
, as this is an array operation.observer_ptr
, as these are iterator operations.observer_ptr
s have strict weak ordering on all implementations, which is not guaranteed for two arbitrary pointers. This is becauseoperator<
is implemented in terms ofstd::less
forobserver_ptr
(as withstd::unique_ptr
andstd::shared_ptr
).observer_ptr<void>
appears to be unsupported, which may encourage the use of safer solutions (e.g.std::any
andstd::variant
)When you need shared access but not shared ownership.
The problem is that raw pointers are still very useful and have perfectly respectable use-case scenarios.
When a raw pointer is managed by a smart pointer its cleanup is guaranteed and so, within the lifespan of the smart pointer, it makes sense to access the actual data through the raw pointer that the smart pointer is managing.
So when we create functions, that would normally take a raw pointer, a good way of promising that the function will not delete that pointer is to use a strongly typed class like
std::observer_ptr
.When passing a managed raw pointer as an argument to a
std::observer_ptr
function parameter, we know that the function is not going todelete
it.It is a way for a function to say "give me your pointer, I will not meddle with its allocation, I will just use it to observe".
Incidentally I'm not keen on the name
std::observer_ptr
because that implies you can look but not touch. But that is not really true. I would have gone with something more likeaccess_ptr
.Additional note:
This is a different use-case from a
std::shared_ptr
. Thestd::shared_ptr
is about sharing ownership and it should only be used when you can not determine which owning object will go out of scope first.The
std::observer_ptr
, on the other hand, is for when you want to share access but not ownership.It is not really appropriate to use
std::shared_ptr
simply to share access because that could be very inefficient.So, whether you are managing your target pointer using a
std::unique_ptr
or astd::shared_ptr
there is still a use-case for raw-pointers and hence the rational for astd::observer_ptr
.One nice consequence of using
std::observer_ptr
over raw pointers is that it provides a better alternative to the confusing and error prone multiple pointer instantiation syntax inherited from C.is an improvement on
which is slightly strange from a C++ perspective and can easily be mistyped as
Yes, the point of
std::observer_ptr
is largely just "self-documentation" and that is a valid end in and of itself. But it should be pointed out that arguably it doesn't do a great job of that as it's not obvious exactly what an "observer" pointer is. First, as Galik points out, to some the name seems to imply a commitment not to modify the target, which is not the intent, so a name likeaccess_ptr
would be better. And second, without any qualifiers the name would imply an endorsement of it's "non-functional" behavior. For example, one might consider anstd::weak_ptr
to be a type of "observer" pointer. Butstd::weak_ptr
accomodates the case where the pointer outlives the target object by providing a mechanism that allows attempts to access the (deallocated) object to fail safely.std::observer_ptr
's implementation does not accomodate this case. So perhapsraw_access_ptr
would be a better name as it would better indicate its functional shortcoming.So, as you justifiably ask, what's the point of this functionally challenged "non-owning" pointer? The main reason is probably performance. Many C++ programmers perceive the overhead of an
std::share_ptr
to be too high and so will just use raw pointers when they need "observer" pointers. The proposedstd::observer_ptr
attempts to provide a small improvement of code clarity at an acceptable performance cost. Specifically, zero performance cost.Unfortunately there seems to be a widespread but, in my opinion, unrealistic optimism about just how safe it is to use raw pointers as "observer" pointers. In particular, while it's easy to state a requirement that the target object must outlive the
std::observer_ptr
, it's not always easy to be absolutely certain it's being satisfied. Consider this example:It may never have occurred to the author of the
replace_last_employee_with()
function that the reference to the new hire could also be a reference to the existing employee to be replaced, in which case the function can inadvertently cause the target of itsstd::observer_ptr<employee_t>
parameter to be deallocated before it's finished using it.It's a contrived example, but this kind of thing can easily happen in more complex situations. Of course using raw pointers is perfectly safe in the vast majority of cases. The problem is that there are a minority of cases where it's easy to assume that it's safe when it really isn't.
If replacing the
std::observer_ptr<employee_t>
parameter with anstd::shared_ptr
orstd::weak_ptr
is for whatever reason not acceptable, there is now another safe option - and this is the shameless plug portion of the answer - "registered pointers". "registered pointers" are smart pointers that behave just like raw pointers, except that they are (automatically) set tonull_ptr
when the target object is destroyed, and by default, will throw an exception if you try to access an object that has already been deleted. They are generally faster than std::shared_ptrs, but if your performance demands are really strict, registered pointers can be "disabled" (automatically replaced with their raw pointer counterpart) with a compile-time directive, allowing them to be used (and incur overhead) in debug/test/beta modes only.So if there is going to be an "observer" pointer based on raw pointers, then arguably there should be one based on registered pointers and perhaps as the OP suggested, one based on std::shared_ptr too.
The proposal makes it pretty clear that it's just for self-documentation:
Is it just for source self-documentation?
Yes.