'Use of observer_ptr
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?
Solution 1:[1]
The proposal makes it pretty clear that it's just for self-documentation:
This paper proposes
observer_ptr
, a (not very) smart pointer type that takes no ownership responsibility for its pointees, i.e., for the objects it observes. As such, it is intended as a near drop-in replacement for raw pointer types, with the advantage that, as a vocabulary type, it indicates its intended use without need for detailed analysis by code readers.
Solution 2:[2]
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 to delete
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 like access_ptr
.
Additional note:
This is a different use-case from a std::shared_ptr
. The std::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 a std::shared_ptr
there is still a use-case for raw-pointers and hence the rational for a std::observer_ptr
.
Solution 3:[3]
Is it just for source self-documentation?
Yes.
Solution 4:[4]
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>
over T*
:
- A default constructed
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.- Pointer arithmetic is not possible with
observer_ptr
, as these are iterator operations.
- Two
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
)
Solution 5:[5]
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.
std::observer_ptr<int> a, b, c;
is an improvement on
int *a, *b, *c;
which is slightly strange from a C++ perspective and can easily be mistyped as
int* a, b, c;
Solution 6:[6]
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 like access_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 an std::weak_ptr
to be a type of "observer" pointer. But std::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 perhaps raw_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 proposed std::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:
struct employee_t {
employee_t(const std::string& first_name, const std::string& last_name) : m_first_name(first_name), m_last_name(last_name) {}
std::string m_first_name;
std::string m_last_name;
};
void replace_last_employee_with(const std::observer_ptr<employee_t> p_new_employee, std::list<employee_t>& employee_list) {
if (1 <= employee_list.size()) {
employee_list.pop_back();
}
employee_list.push_back(*p_new_employee);
}
void main(int argc, char* argv[]) {
std::list<employee_t> current_employee_list;
current_employee_list.push_back(employee_t("Julie", "Jones"));
current_employee_list.push_back(employee_t("John", "Smith"));
std::observer_ptr<employee_t> p_person_who_convinces_boss_to_rehire_him(&(current_employee_list.back()));
replace_last_employee_with(p_person_who_convinces_boss_to_rehire_him, current_employee_list);
}
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 its std::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 an std::shared_ptr
or std::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 to null_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.
Solution 7:[7]
Aside from the documentation use case, there are real world issues that can happen when passing around raw pointers without the observer decoration. Other code may incorrectly assume lifetime responsibility for raw pointers and pass the pointer into ownership taking std::unique_ptr
, std::shared_ptr
, or just plain dispose of the object via delete
.
This is especially true of legacy code that maybe being upgraded where ownership rules aren't fully established. The observer_ptr
helps enforce the rule that the lifetime of the object cannot be transferred.
Consider the following example:
#include <iostream>
#include <memory>
struct MyObject
{
int value{ 42 };
};
template<typename T>
void handlerForMyObj(T ptr) noexcept
{
if (42 != ptr->value) {
// This object has gone rogue. Dispose of it!
std::cout << "The value must be 42 but it's actually " << ptr->value << "!\n";
delete ptr;
return;
}
std::cout << "The value is " << ptr->value << ".\n";
}
void func1()
{
MyObject myObj;
MyObject *myObjPtr = &myObj;
myObj.value = 24;
// What?! Likely run-time crash. BOO!
handlerForMyObj(myObjPtr);
}
void func2()
{
MyObject myObj;
std::observer_ptr<MyObject> myObjObserver{ &myObj };
myObj.value = 24;
// Nice! Compiler enforced error because ownership transfer is denied!
handlerForMyObj(myObjObserver);
}
int main(int argn, char *argv[])
{
func1();
func2();
}
In the raw pointer case, the incorrect deletion of the object might only be discovered at runtime. But in the observer_ptr
case, the delete
operator cannot be applied to the observer.
Solution 8:[8]
Others have pointed out various benefits of observer_ptr
besides self-documenting non-ownership. However, in case you are only interested in conveying non-ownership, Bjarne Stroustrup suggests a succinct alternative for std::exprimental::observing_ptr
in the C++ standards working group paper P1408R0 (where he, incidentally, proposes to abandon std::observer_ptr
):
template<typename T> using observer_ptr = T*;
Solution 9:[9]
I think you need this -- noshared_ptr / noweak_ptr
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Barry |
Solution 2 | |
Solution 3 | Community |
Solution 4 | |
Solution 5 | Richard Forrest |
Solution 6 | Noah |
Solution 7 | Robin R |
Solution 8 | |
Solution 9 |