There are objects and there are objects. Value objects, which
can be copied, are not a problem, since you don't normally have
pointers (smart or raw) to them, anywhere. Entity objects do
have identity, and you can (usually) navigate between them.
They normally have their own lifetime, defined by the
application logic, however, and aren't in containers, per se.
(Pointers to them may be in containers, for navigation purposes,
of course.)
So major objects that have complex state machine have usually lot
of other things to worry about. I was more about the typical things
that have very narrow set of responsibilities. Those are the most
numerous ones. Not the "device" but its "parameters", not an "account"
but its "transactions", not the "dialog" but its "controls".
Polymorphic, many to one, with their life-time bound to the owner.
None of these smart pointers implement relationships correctly.
A raw pointer does, sort of. (You still need an observer,
because there's a good chance that an object which has a pointer
to you will want to know when you are destructed.)
Observer pattern is not needed always. Typically it is needed in
places where wills of several active parts meet. Observer pattern
helps to orchestrate events in such places greatly. Most of code
still deals with far more mundane issues. I understand that we
agree that on these mundane cases smart pointers save lot of time.
Navigation, like going from one object to another. Implementing
the application specific relationships between objects. In
a GUI, for example, a container will know about its
sub-components, the sub-components will know about the
container. You can navigate in either direction (and in some
cases, accross).
You mean sub-components in GUI ... like shapes on diagram? There
is strict ownership relation, shapes can *not* continue being when
the diagram is closed. So diagram could have shared_ptr and all
the rest parts who are interested in particular shape a weak_ptr.
Anyway ... I see your point, weak_ptr has inconvenient interface
and carries a slight inefficiency burden with it so if pointing object
anyway tracks pointed at object's life-time by other, not-ref-counted
means then here is waste.
You made me inspired to write a non-owning 'track_ptr' that is
better integrated to the logic of such "other, not-ref-counted
means".
I already have 'robust_ptr' that is like 'unique_ptr' used for
polymorphic base object but in all situations when 'unique_ptr'
is nullptr it instead points at static (internally immutable)
"Missing" object that implements the polymorphic interface. As
result ... it may be always dereferenced.
I will couple the 'track_ptr' as optional side-kick of my
'robust_ptr' because the effect is great so far. Then diagram could
have 'robust_ptr' to shape and all the rest could have 'track_ptr'.
'unique_ptr' seems is currently also lacking such optional non-owning
side-kick. I have used reference or std::reference_wrapper to object
so far.
Observers are a perfect example. Observers can't use
references, since they have a dynamic lifetime. In a typical
implementation, the Observee will have a vector of raw pointers
to the Observers. How else could you implement it.
Observee has set of signals. Usually a fixed set signaling about
interesting events like state transitions.
Each signal may have some slots (function objects related to Observer)
connected. Connections may be optionally made to track Observer using
'weak_ptr'. If to add 'track_ptr' then it becomes "either with 'weak_ptr'
or with 'track_ptr'".
You can't
use references (a vector can't contain references). You can't
use unique_ptr (because the observer objects exist outside of
the observee). You can't use shared_ptr (because the observer
objects manage their own lifetime, and will auto-destruct). You
can't use weak_ptr, because there cannot be any shared_ptr to
the object. You can't use iterators, because the object itself
isn't in any container.
Ok, in the building blocks the raw pointers are always needed, be it
iterator, container or smart pointer. Such building blocks are written by
the brightest guys and those use various concurrency and exception
safety testing frameworks for testing and profile very well, so ... I am
not worried that things like shared_ptr somehow fail me. I am trying to
move the raw pointers into such template libraries, away from abstraction
layer of ordinary C++ Joe.
A long time ago, I worked with a firm which had designed
a managed_ptr; a pointer which was, itself, an observer, and
reset itself to null when the pointed to object was deleted.
This was designed to solve a very special case, and worked well
there, but experimentation soon revealed that it wasn't
a general solution. Whoever had a pointer to the object usually
needed to know when the object was destructed anyway, so it
could reconfigure itself to function without the object. And
once you have the observer, any additional baggage just to
manage the pointer is just added complexity.
When the relation is non-owning and lifetime is unrelated then non
-owning pointer like 'weak_ptr' is best. I see your point about
missing smart pointer now very well. 'shared_ptr' is for cases
when ownership is shared. When there is single owner then it is
(usually insignificant) waste. Unresolved Problem is that weak_ptr
is awkward to use and so its usage adds to complexity.
Of course not. std::vector is a low level implementation
detail. It only exists to be used by higher level objects.
Ok, but I am still big fan of intrusive containers. It works well on
cases when there are no observation needed.
Obviously. An application isn't defined in terms of
std::vector.
It might help in a few cases, but it still doesn't solve the
larger problem. Removing the pointer from a vector or a map is
one thing; telling the object which contained the vector or map
that something is happening that it needs to know about is
another. And once you've solved this second problem, you don't
need a solution specialized in removing the pointer from
a container.
Yes, it does not solve the cases where you actually need signals
for some other reason. It just is very efficient even when there
are signals for some other reason. Element of intrusive container
knows exactly where it is (if it is), so erasing itself is O(1).
And intrusive containers suppose that the object knows about all
of the containers it might be inserted into. Which is
fundamentally impossible, because it is an open set.
That is untrue in C++. We have always finite number of classes and
therefore we have finite number of 1->n relations. It is by design
of language, we can't add classes (and so relations) between
classes dynamically, run time.
When there is B n<->m C relation then that clearly indicates
under-engineering. Class X that carries the relation between B and C
is missing. While this may be actually case in real code (who has not
seen broken designs?) I would certainly deal with it by adding missing X,
not by cleverly using raw pointers.
[...]
Polymorphic objects are usually not copy-able but often clone-able
and are fine with smart pointers.
Some polymorphic objects, yes. Larger, entity objects will
contain pointers to other objects, which in turn contain
pointers back to the original object, and have their own
specific lifetimes.
Ok, I see that.
The smart pointers of C++ are *not* in any way opaque. In my
external interfaces, I make extensive use of opaque pointers:
void*. That's the only opaque pointer in C++. (But of course,
I don't want opaque pointers internally. They're only used for
information hiding at the boundary level.)
Yes, and robustness of boundary between modules is very important
topic. If modules are developed by different teams then robustness
of interface helps to dispatch on what side of boundary the problem
lies. That saves lot of time.
'unique_ptr' may point at opaque type where it is constructed but
not where it is destroyed. 'shared_ptr' may point at opaque type
where it is destroyed but not where it is constructed. Non-owning
pointers (like weak_ptr) may be opaque all the time.
[...]
I have to admit that I also have several C style arrays that are
entirely immutable. It is because they work and I have other things to
worry about.
Most of the time (in my applications, at least), if I'm not
using std::vector, it's because I have a static array with
static initializers, and I want the compiler to count the
initializers for me. Replacing them with std::array won't work.
(And yes, they're always const.)
Ok, that is not issue with me. I do same and see no problem. Maybe
some day when I have nothing to do I deal with it just for sake
of purity. The time is usually better spent dealing with real issues
not issues raised from following pure concepts too closely.
In other applications... The cost of using an std::vector with
three elements) for the coordinates of a Point3D, when you have
thousands of them, and are constantly copying them, could be
prohibitive. In this case, std::array could be an option
(although I'm not sure what it really buys you over a C style
array with 3 elements).
On such cases there is an option of having Point class with members
x, y, z and static std::array of pointer-to-member. The x,y,z are
just the coordinates of a Point in room. Like the red, green, blue,
alpha or hue, saturation, lightness, alpha are components that mutually
just describe the Color of something.
The applications I'm talking about where you need mostly raw
pointers tend to be the larger ones.
From small applications that are well integrated with each other one can
make rather complex software. We sometimes forget it when using C++.
It depends on what you're doing. shared_ptr is extremely
dangerous if used inappropriately. And naked pointers are
perfectly safe when used for navigation.
Everything can be misused. Nothing is 100% fool proof. I merely meant safety
of detecting more possible misuses compile-time. That saves time.
The problem is that smart pointers are in many cases *more*
dangerous than naked pointers. (Especially with auto. Consider
what happens with:
auto objPtr = somePtrVector;
If somePtrVector contains unique_ptr, you're in for a surprise.)
What surprise? I expect diagnostic here. same with:
auto objPtr = somePtrVector.front(); // expect diagnostic
§ 20.7.1.2:
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
Maybe conforming compiler may do something surprising here but how?