|
Boost : |
Subject: [boost] [smart_ptr] Interest in the missing smart pointer (that can target the stack)
From: Noah (duneroadrunner_at_[hidden])
Date: 2016-01-28 12:20:08
Hi all, I'm new here. Here goes:
I propose a new smart pointer called, say "registered_ptr", that behaves
just like a native C++ pointer, except that its value is (automatically)
set to null_ptr when the target object is destroyed.
This kind of smart pointer already exists and is being used in the form
of, for example, QPointer and Chromium's WeakPtr
(http://doc.qt.io/qt-5/qpointer.html and
https://code.google.com/p/chromium/codesearch/#chromium/src/base/memory/weak_ptr.h),
although those implementations only apply to a specific set of object
types. There's also std::experimental::observer_ptr
(http://en.cppreference.com/w/cpp/experimental/observer_ptr), which
seems to be trying to serve a similar purpose, but with none of the
functionality.
This might be as far as you need to read to decide if you think it's a
good idea or not. The rest of this (kind of long) post addresses some of
the details and motivation.
It's instructive to compare this "registered_ptr" with
(std::)shared_ptr. Both have a (modest) performance cost, although
registered_ptr's performance cost is significantly lower, as will be
discussed later. Both can have a significant "code safety" benefit when
used for that purpose - i.e. reduce or eliminate the possibility of
invalid access of an object that has already been destroyed. And both
have a functional purpose other than safety or convenience (or just
being a regular pointer). That functional purpose is to deal with
situations when it is not easy to predict in advance (i.e. at compile
time) when an object will be referenced (and stop being referenced).
shared_ptr deals with these type of situations by ensuring that the
object remains "alive" for as long as there are any (owning) references
to the object. This assumes that the object has no "curfew". But one can
imagine situations where an object needs to be destroyed at or by a
certain time or event. If the object is exclusively holding an in demand
resource, or security lock for example. In these cases one might prefer
that late attempted references to the object fail (safely) rather than
have the object's destruction delayed. registered_ptrs might be more
suitable for these situations.
So from this comparison it's hard to argue for the inclusion of
shared_ptr and the omission of registered_ptr in the library.
registered_ptr can be thought of as a kind of "non-intrusive"
counterpart to shared_ptr. "But wait", you say, "shared_ptr already has
a non-intrusive counterpart. Namely weak_ptr." Yes, but the difference
between weak_ptr and registered_ptr is that registered_ptr is
independent, not just an appendage of shared_ptr. And registered_ptr can
point to stack allocated objects (just like native C++ pointers can),
but at the cost of "thread safety". Referencing an object on another
(asynchronous) thread's stack is inherently not safe, right? Some may be
concerned with this lack of thread safety - I am not, I don't think we
should be encouraging casual asynchronous object sharing - but the
performance dividend of getting to put the target object on the stack
rather than the heap more than makes up for it in my opinion.
So I've actually already had cause to implement, and use, a version of
this "registered_ptr" (I actually called it "TRegisteredPointer"), and
in practice it works great. But there are, arguably, a couple of issues.
Besides the thread safety (non-)issue, my implementation can only target
types that can act as base classes. By default most classes can act as a
base class, but native types like int, bool, etc. cannot. In my case,
this is not a big issue because TRegisteredPointer is actually part of a
small library that includes (safer) substitutes for int, bool and size_t
that can act as base classes.
The good thing is that it does seem to serve its purpose of not only
being safe, but also faster than shared_ptr. Much faster when targeting
stack objects as one would expect, but also faster when targeting heap
objects according to my simple benchmarks (msvc2013 - Windows 7 - Haswell).
My implementation can be found here:
https://github.com/duneroadrunner/SaferCPlusPlus. It's in the file
called "mseregistered.h". Examples of it in action can be found in the
file called "msetl_example.cpp", near the end. No decent documentation
yet, but the examples are commented and it should be clear what's going on.
So far I've posited that the argument for registered_ptr is at least as
good as the argument for shared_ptr. But let me make a further argument
for why I think it's particularly important to include registered_ptr.
First, let me make an argument for "safe" pointers in general. By safe
pointers, I mean pointers that have zero chance of participating in a
dereference to invalid memory. This will usually imply that the safe
pointer will do a runtime check and throw an exception if an invalid
dereference is attempted. Now, I subscribe to the C++ "trust the
programmer" philosophy, so I agree that programers should have the
option of avoiding these runtime checks. But security (and by extension,
language safety) is so important these days that programmers should also
have the option of having these runtime checks when desired. So I'm
suggesting that all the smart pointers (shared_ptr, unique_ptr, etc.)
come in "safe" versions as well. There is already some precedent for
safe and unsafe options. For example, std::vector<> provides both the
"unsafe" [] operator and the "safe" at() function. And ultimately it
needs to be properly measured, but I would think that the performance
cost of this kind of runtime check should be quite modest. There is
plenty of opportunity for optimizing compilers to strip them out when
not needed and of course these sorts of runtime checks are a favorite
food of branch predictors in modern cpus right? I haven't put a lot of
effort into this, but my simple benchmarks show checked dereferences
being only modestly slower than unchecked dereferences. (The
"msetl_example.cpp" file includes some simple benchmarks.)
So why does this make registered_ptr so important? Because the "safe"
version of registered_ptr would be the safe pointer with the lowest
performance cost. Perhaps low enough for the safety benefits to be
widely considered worth the cost.
Of course the ideal would be smart pointers that could distinguish
between stack allocated objects and heap allocated objects.
Unfortunately, as far as I know, (standard) C++ doesn't seem to give us
that facility. If we can't have those, I think we need a smart pointer
that can at least accommodate stack allocated objects. Right?
Noah
Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk