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.