"Nick Keighley" <
[email protected]>
Not sure how you arrived at the everything is FIFO assumption.
I was stating to use managers -- and that managers can be
non-locals. After all we don't have so much places, so if you
have a manager it can be:
- local in a block in the immediate function
- local in a block in a function somewhere up the call chain
- a global(/singleton)
The destructors of those all occur at very specified points in
time. Specified points which don't necessarily (or even often,
in a lot of application domains) with where the program's
requirements specification say that the object's lifetime ends.
you put responsibility of deallocation to the manager, and it
will eventually happen for all of them.
Not necessarily. The typical server runs in an endless loop.
Variables in the same scope as the loop and global variables are
never destructed. Even if you reboot the server from time to
time, you can't wait until then to delete things like call
records (supposing a telephone switch or transmission
equipment).
There is a chance for pseudo-leaks, we probably not yet
discussed.
I'm not sure what you mean by "leak" and "pseudo-leak". If we
take the earlier example of a call handling system, if any
resources associated with the call are not released when the
call is terminated, it is a leak.
Like when you have a list with long life, keep adding nodes
and never remove any.
If you continue to need to access those nodes, you don't have a
leak, pseudo- or not (although you may have a requirements
specification that is impossible to implement on a machine with
finite memory). If you don't continue to access those nodes,
then you have a leak---there's nothing pseudo- about it, since
it will bring the system down (no pseudo-core-dump, but a real
one) sooner or later.
Stuff can get stuck in managers that are global or live wery
much up (say right in main()). Getting the resources released
only at prog exit may be not good enough, and act like a
regular leak in all practical terms. No silver bullets.
Think that is hardly news to anyone reading this group.
Getting resources released only at program exit is *not* good
enough. His example was call handling: program exit probably
won't be for a couple of years or more, and the system might be
handling 10's of thousands of calls a day.
And pseudo-leaks are damn hard to discuss in abstract, in a
forum: how you tell whether some such node is yet needed in a
system or not? Except by the particular design? ;-)
That's true for most objects.
The point is that the managers in the latter category are not
flooding the app -- you have only a few of them hopefully, and
can pay close attention on the content. And normally they are
helped out with more local RAII sub-managers.
Many real systems work processing 'requests', serving events
from outsude. That have a well defined point of entry, that
is fortunately a function/stack frame too. And you can use
that to manage all the stuff that is local-to that request.
When it ends, you can be sure the app's state is as before.
Or changed *only* as expected by the req itself.
Yes. And the expected change, in some cases, is that a new
object was created or that an existing object was destructed.
The more problematic RL cases when request create a "session"
to work with and must carry state of that further. So you
will have a session manager upwards.
That was basically his example. A call manager is really a sort
of a session manager. (Unlike most "sessions", a call typically
involves several parties, and the session only ends when the
last partie leaves it.)
Normally some of the further events will bring the session to
its final state where it can be disposed -- then certainly
releasing all associated resources. If driven by outside
events, certainly only a proper design can make sure it
happens consistently.
Certainly. And in the absense of such cases, when do you use
dynamic memory?
Still is is not at all stupid, and it works perfetcly fine in
my practice. Possibly my separation of things to "client" and
"library" (aka framework, support, etc) is not clear and
misleads judgement.
The most power of C++ comes from the fact it gives superior
tools to create a quasi-language in which you can most
directly and expressively state the real aim of the program.
To em the client or application code is what is written in
that latter language. That definitely don't have place for
keyword delete.
The keyword "delete" is a red-herring. Suppose a call object is
processing a "hang-up" event, and it determines that this event
corresponds to the last party in the call. Whether it does
"delete this" or "manager.please_please_delete_me(this)" really
doesn't change anything. (Actually, it does. When you see
"delete this" in the code, you know that the only thing you can
do is return. It's more tempting to continue processing after
something like "manager.deenrol(this)".)
Like in these very cases. Client code shall no way attempt to
implement containers, string, etc -- but use them for good.
It asks dynamic stuff in indirect way, like resize, or
invoking factory calls.
I think we all agree here. There are two major reasons to use
dynamic memory: the object's lifetime doesn't correspond to any
of the predefined lifetimes (local or global), or the object's
size or actual type aren't known at compile time. For the
second, I don't think there's any real disagreement: you use
some sort of "container" (preferrably the standard containers)
to manage the memory. For the second, the issues are more
complex: a transaction typically does correspond to (or end at)
an established scope, a connection only does if the server
dedicates a thread to each connection (which for various
reasons isn't really an option in call handling). In such
cases, you might want to use something like auto_ptr to hold
any objects until you commit, but once you've committed, there's
no real point, and even a few drawbacks, to using some special
manager object to avoid an explicit "delete".
Eliminating dynamic memory use is rare -- probably extinct
outside embedded.
Come now, you don't use "new int" when what you want is an int
with the same lifetime as the function. Practically, because
int is a value object which supports copy, you don't use "new
int" ever. (About the only time you might dynamically allocate
a value object is when it is big enough to make copying too
expensive. Otherwise, you copy.)
I'm beginning to form the rule of thumb that all uses of
new[] are errors. Or at least need looking at very hard.
Absolutely. delete[] was the first thing I blacklisted and it
certainly dragged new[]. And didn't meet a single case it was
needed in the last 15 years.
Beat you there
. I've never used a new[] or a delete[], and
I've been programming in C++ for 17 or 18 years now.
[simple example] (too simple if you ask me)
think a program that processes text manipulating
strings all its time. A C++ solution would use
std::string, (or any of the much better string
classes). Doing all the passing-around, cutting,
concatenating, etc.
consider a text editor that allows the user to delete
text. Who deletes the string that holds the deleted
stuff and when[?]
That's probably not a good example. The text buffer holds
the text before it's deleted, and the deleted text itself
is never a separate object, unless...
sorry, "who deletes the text buffer?"
In a single-doc editor the text buffer can be permanent.
(I guess text editor is not a good forum example, the
questions you rised can be easily answered with a multitude of
alternative designs, all with pros&cons, yet trivially
ensuring resource control -- but it would take a book-worth
amount of words. If really needed, pick some sub-part, or
narrow it down.)
Undo also can be made with several approaches, like keeping
snapshots of the current state, or record request to play
forward or back.
What you mean by "RAII alone"? Possibly we speak on distict
tracks because you fill that term by different content...
The meaning tied to original mosaic is actually rearly meant
literally -- the most common use is RRID (res. release is
destruction) leaving allocation freeform -- still routinely
referred as RAII. :-o
Yes. The name is historical, and actually slightly confusing,
because what characterizes RAII is the release in a destructor.
But the name is ubiquious, so we're more or less stuck with it.
But that doesn't change his comment: the decision as to when to
"drop" an redo record (regardless of how the information is
stored) will almost certainly not be made in a destructor.
IMO discounting UB and other blacklisted cases they are.
I was thinking about things like shared_ptr, which are
definitely undeterministic. More importantly, of course, the
points in which a destructor are called are very limited (except
for explicit deletes), and often don't occur in places related
to the required lifetime of the object.
If you mean the "is-initialization" part, sure, it covers only
a subset of cases.
He explicitly said "is released". He's not mentionned creation.
CallManager:
rocess_event (std::auto_ptr<Event> event)
{
std::auto_ptr<Call> call(0);
if (is_new_call (event))
{
call = CallFactory::create_call (event);
CallList::add_call (call);
}
else
{
call = CallList::lookup_call (event);
}
call->process_event(event);}
This does not make much sense to me, id call_list is the
manager/owner of calls, you don't use auto_ptr like that, and
especially don't place the result of lookup in auto_ptr.
There must be exactly one owner. Maybe you meant shared_ptr,
but it looks like overkill too.
Yep. For this, a raw pointer is the most appropriate solution.
(Long term. I'd use an auto_ptr between the
CallFactory::create_call and the successful addition of the call
to the list.)
In the case of a call, the "owner" is the call itself. No other
owner makes sense.
And this is a good example what client code shall NOT do by
any means -- leave call management with the call manager. So
the whole thing becomes a one-liner:
get_call within the manager can see whether it is existing or
a new call, create as necessary and return one by ref.
Maybe. There are different levels, and at the lowest level, you
don't want to rewrite std::map so that it will handle creation of
new objects. Note that Call is probably a virtual base class,
and CallFactory will create different derived classes according
to information in the event. And of course, what makes it a new
call depends on data in the event as well, and possibly the
state of other objects.
The example is considerably simplified. Consider the off-hook
event, which occurs when you lift the receiver off the hook: if
the current status of the terminal is "ringing", then you
process the event with the call object which generated the
"ringing" status; if the current status is inactive, you create
a new call object. In at least one possible design, you would
in fact first obtain an equipment object from the event, and ask
it for the call object. But once created, the call object
manages itself.