Am 05.06.2012 19:26, schrieb Zeljko Vrba:
>> Can you? What happened with the object under destruction?
>> Apparently, it is not properly destructed, otherwise there wouldn't
>> be an exception, yet you have no longer a way of accessing this
>> object and trying a different clean-up strategy.
>
> You can wrap the relevant (not-yet-destructed) part of the object into
> the exception object and try to deal with it in catch. For example,
> if the object wraps a file, you would transport its file-descriptor
> through the exception to the catch site.
And where take you this object from? After all, its construction might
also trigger an exception, let it be due to memory exhaustion.
>> That handling cleanup errors (and others) requires cleanup of
>> objects.
>
> I'm not sure I get it. If in `delete x;` the destructor throws, is
> the memory freed? This is basically the only piece of work that the
> program can't do itself.
I don't remember what the standard has to say about that, but at least
the stack is unwound, so objects on the stack are most certainly gone.
Objects on the heap might or might not go, but what would you do with a
pointer to such an object? There are no longer any class invariants you
could depend on since the object is already destroyed by the very means
of the C++ standard.
>> This actually means, however, that you should better replace
>> throwing in the destructor by an assert, or instead build an
>> alternative
>
> assert() should be understood as no-op, so it's not adequate
> replacement.
Huh? assert() is certainly not a no-op. It is a mechanism to check for
broken class invariants due to bugs in the code.
>> cleanup strategy into the destructor that can happen the error
>> locally.
>
> In general this means that the 1) object needs to know where it was
> called from, or 2) I need many subclasses which differ only in their
> destructors.
If you need the context to handle the error, then a throwing destructor
is neither the solution since there is nothing you could do with the
object in first place. It is no longer in a valid state, so the caller
cannot do anything about it. If you need caller assistance on an error,
you need to provide a second method for cleaning up the object state,
something like "close()". Then you can ensure, by a proper object
design, that the state of the object is still such that you can do
anything about it. However, after the destructor is called, the object
is gone.
For the problem you have a throwing destructor is not the solution.
>> The problem is that you cannot delegate the handling of cleanup
>> errors as with other errors since the object that caused the problem
>> is already no longer reachable.
>
> You can copy the relevant part of the object and pass it as the part
> of the exception.
Where to?
>> Are they? There are many error reporting algorithms, amongst them is
>> also assert().
>
> Why do you believe that crashing on assert() [assuming it's not a
> noop] is better than std::terminate being called?
Whether assert() is "better" than "terminate" depends on the situation.
Assert is better for reporting errors in the program logic, i.e. a
program should run into an assertion whenever the code detects a state
that should logically not exist, but does due to some defect in the
code. It expresses this situation correctly.
terminate(), on the other hand, just terminates. Assert() could use
operating system means to report the error to the user and get some
debug output. For example a core dump. That is usually more helpful *in
such situations* that just terminating the code.
Terminate might be better in other situations. It depends. One can
barely say "one is always better than the other".
>> If you throw an exception, the point is that there is somewhere a
>> catch() that can deal with the situation - otherwise, you wouldn't
>> throw in first place. That's quite logical. However, if by language
>
> That may be the point, but it's not the *whole* point. For example,
> some STL algorithms throw (e.g., std::string methods throw on invalid
> indices), but I never write corresponding catch. End result: if my
> program passes invalid index (i.e., it is buggy), it crashes, just as
> it would after assert.
Wait a minute - that rather depends on its use. operator[] of the string
does *not need to* throw since it doesn't have bounds checking
guaranteed by the specs. If the index is out of bounds, the
implementation may do whatever it seems fit. Terminate, assert, nothing.
std::string::at, however, does throw. Thus, if you need bounds checking,
you can have it by using the appropriate function, but only then.
> STL is thus an example of using exceptions as "always on" substitute
> for assert().
Sorry - no, that's simply not correct. I don't know what your
implementation does, but that is surely not required by the specs for
operator[].
>> construct, you *cannot* deal with the situation since the object to
>> deal with is no longer reachable, the catch is superfluous in first
>
> The exception object can transport the relevant state of the deceased
> object. I'm repeating myself from the last post, but it seems that
> this point hasn't come across since you're repeating this.
I simply don't consider this a valid argument. Look at the design you
have: You delete the object, thus tell the code "please get rid of this
thing". Yet, what you get is a new instance of a partial object. So what
exactly does this mean? The object didn't want to die, so the attempt to
delete it wasn't justified. Wait a minute - we could have done that in a
simpler way: First try an orderly shutdown ("close()") which we can
handle *without* loosing the object, and then when we know that the
Parrot is really dead, bury it.
>>> The real question is: should destructors be used to enofrce
>>> invariants? If yes, how do you report inability to fulfill
>>> invariants? If not, what about RAII -- in this case the
>>> applicability of RAII suddenly gets *very* narrow.
>
>> I don't quite follow.
>
> As noted in another post, RAII is often used as generic scope-guard; a
> callback to be executed on scope exit. If we limit us to dtors being
> used ONLY for operations that "cannot fail", this 'other' use of the
> RAII idiom is invalid.
It means that you cannot use RAII where the object release may create an
exception, exactly. Why is that a problem? Typical example: RAII
includes locking a mutex. If I cannot *release* the mutex, something is
severely broken. In such a case, I should better abort the program
(here, assert() is probably not the right solution since it is not an
invariant at the specific location that created the problem, but memory
corruption somewhere, or anything major).
>> Is this truely the case? There are several strategies for error
>> handling in destructors right away, but exceptions aren't. This does
>> not mean that destructors aren't able to guarantee invariants. Just
>> that exceptions are not the right mean to deal with problems when
>> handling with them.
>
> Of course, but a generic class can't know how to handle the error,
> which leads us to the solution of subclassing just for the sake of
> defining destructor behavior in the case of failure.
No, not at all. It means that you need to design your classes carefully.
A destructor is not the location for major work that could fail - except
for bugs in the program flow. And those you should report by other means.
>> Well, it depends: If I write a file to the system, a possible
>> solution would be to remove the file in question to ensure that the
>> user never sees incomplete and potentially corrupt data. When
>> reading a file, a
>
> Filesystem may get corrupted so that even remove doesn't succeed.
We're drifting off, that's not the point. Even then, throwing an
exception in such a situation wouldn't help either.
>> error should be handled in a different way - well, don't put
>> fclose() into the destructor because then you still *need* the
>> object to resolve the situation, so closing and destruction are two
>> different things.
>
> Agreed, BUT: many textbooks gloss over this issue: they emphasize that
> it's better to use fstream instead of FILE* also because you can't
> forget to close the file -- it happens automatically.
It does happen automatically, but it cannot handle an error by a
catch(). Let's discuss this case a bit, ok? What can you do if the close
fails?
a) If the file is not important, either leave it alone or remove it.
b) If it is feasible to inform the user about the event, you could
create the error requester in the destructor - or -
c) you could create a separate close() function which does the job
whenever you need to report about the issue, and use the destructor as a
convenience operation when it is not so important.
What can do you about a failing fclose() in first place?
> So the widely-
> marketed RAII style of coding
>
> void f() {
> fstream f;
> // closed at scope exit
> }
>
> becomes
>
> void f() {
> fstream f;
> try {
> // some code which may throw
> } catch(...) {
> f.close(); // may throw; "forget" the other exception
> // maybe wrap in try-catch and std::terminate?
> throw; // in case close succeeds
> }
> f.close(); // duplicated; c++ doesn't have finally{}
> // may throw; we don't care
> }
>
Not really. First of all, this code doesn't solve the "double fault"
problem either, nor the throwing destructor. At some point, you must say
"ok, dear user, we did all we could to keep your files intact, but at
this time we need to give up". Second, the first code does not include
error handling, so it is not an argument for your point. It is a quick
solution that might be good enough - but it neither gets any better by
throwing in the destructor because you wouldn't gain anything.
Second, typically you would place this "unchecked fclose" which you have
now explicitly spelled out in the code into the destructor, exactly as
it was before, but *in addition*, would call f.close() where you *could*
deal with the problem.
So the 'we don't care about the file' solution 1)
{
fstream f;
...
}
would remain valid, but wouldn't ensure that any cleanup work is done
when the fclose fails. Might or might not be ok, depends on the situation.
If you *must* deal with errors, then
{
fstream f;
try {
... do all the file operations...
f.close(); // I'm done
} catch(...) {
// handle my errors
}
}
is exactly the right solution because you *still* have access to the
file object within the exception handler, but you do not have to spell
out the "I do not care" fclose you have above, but you need to put
somewhere in the program anyhow, no way around it.
>> Huh? Not at all. Just that the above program is flawed, and the
>> design isn't right. If you need a strategy for dealing with the
>> failure, don't put this operation into the destructor - otherwise
>> the object state for recovery is gone. Then we're back to something
>> looking to me worse than the error-code style, see the example
>> above.
>
> Then we're back to something looking to me worse than the error-code
> style, see the example above.
I afraid you still didn't get the design right. I spelled it out
explicitly how it *should* look.
Greetings,
Thomas