Checking return values for errors, a matter of style?

M

Morris Dovey

Johan Tibell (in (e-mail address removed))
said:

| I've written a piece of code that uses sockets a lot (I know that
| sockets aren't portable C, this is not a question about sockets per
| se). Much of my code ended up looking like this:

<"succeed or die" examples snipped>

| What's considered "best practice" (feel free to substitute with:
| "what do most good programmers use")?

IMO, "best practice" is to detect all detectable errors and recover
from all recoverable errors - and to provide a clear explanation
(however terse) of non-recoverable errors.

I have difficulty imagining that any "good programmer" would ignore
errors and/or fail to provide recovery from recoverable errors in
production software.
 
R

Richard Heathfield

Andrew Poelstra said:
What exactly /would/ be the way to do such a thing? I ask you because
you don't like multiple returns or the break statement, both of which
would be a typical response.

Well, the check is done in the obvious way:

p = malloc(n * sizeof *p);
if(p == NULL)
{

The response to a failure depends on the situation. I've covered this in
some detail in my one-and-only contribution to "the literature", so I'll
just bullet-point some possible responses here:

* abort the program. The "student solution" - suitable only for high school
students and, perhaps, example programs (with a big red warning flag).
* break down the memory requirement into two or more sub-blocks.
* use less memory!
* point to a fixed-length buffer instead (and remember not to free it!)
* allocate an emergency reserve at the beginning of the program
* use virtual memory on another machine networked to this one (this
is a lot of work, but it may be worth it on super-huge projects)
Being as every other post was pretty much exactly as insulting as this,

Well, I wasn't really trying to insult you. I was just trying to communicate
my concern at the prevalence of this really bad practice of not checking
that an external resource acquisition attempt succeeded. You wouldn't omit
a check on fopen or socket or connect, so why on malloc?
I'd say that I wasn't not wrong on any minor point! I'm glad that I
haven't had the chance to make these foolhardy changes to my actual code
yet.

So am I. :)
 
J

Johan Tibell

Morris said:
IMO, "best practice" is to detect all detectable errors and recover
from all recoverable errors - and to provide a clear explanation
(however terse) of non-recoverable errors.

I have difficulty imagining that any "good programmer" would ignore
errors and/or fail to provide recovery from recoverable errors in
production software.

The topic of this post is not whether to check errors or not. Read the
original message.
 
R

Richard Tobin

Why would you want to settle for 99.99% when you can get 100%?

Of course, as noted in previous threads in this newsgroup, checking
malloc() results will only increase your success rate from 99.99% to
99.991% on the many modern operating systems which overcommit memory.

On the other hand, if someone uses your code in a situation where it
Really Matters that it fail cleanly, they will presumably take steps
to ensure that malloc() doesn't overcommit.

-- Richard
 
S

Skarmander

jacob said:
Keith Thompson a écrit :

Ahhh... You do not use debuggers very often?

Mmmm... Well, they are great tools. I use them very often,
actually I spend most of the time either in the editor
or in the debugger.
That's a common but by no means universal approach to coding. Personally, I
find that coding as if my system had no debuggers results in code for which
I don't need a debugger, and this is why I don't use debuggers often.

I think that this style of coding is overall more speedy than the one where
you do assume debuggers are readily available for assistance, but I have no
hard data on it, and doubtlessly it will vary by individual.
The only time when I did not use a debugger was when I was writing
the debugger for lcc-win32. My debugger wasn't then able
to debug itself so I had to develop it without any help, what made
things considerably more difficult...
Well, you could have used a different debugger... Or, like compilers
bootstrapping themselves, you could have used a rudimentary but easily
eye-proofed version of your debugger to develop the rest with.

S.
 
J

jacob navia

Skarmander said:
Well, you could have used a different debugger... Or, like compilers
bootstrapping themselves, you could have used a rudimentary but easily
eye-proofed version of your debugger to develop the rest with.

To use a different debugger I would have to be sure that the
compiler generated the right debug information, that the linker
linked (and compressed it) OK.

That was the last and most difficult task I did for my system,
and it really was a lot of pain because at the start I had only
printf as a tool. Any bug in the compiler, assembler or linker
in the handling of the debug information would appear there!

After I got the debugger running more or less I started using
the Microsoft debugger that to my amazement did manage to
read through the sometimes buggy debug information without
crashing!

THEN it was much easier and I could start caring about user interface,
etc.

Now my debugger is able to debug itself, and I could bootstrap
the 64 bit version with much less effort.

but we are steering off topic and I fear the regulars coming :)
 
A

Andrew Poelstra

You've forgotten the rule of computing probability - if there's one
chance in a million, it happens every second.

You must have written some of the code for Windows.

I made a typo in a malloc call and the thing BSOD'd. Haven't done any
real programming in Windows since.

It's nice not working for an corporation that imposes systems on you.
I'm pretty sure that I should enjoy this while I can.

(As to the original post, I've mentioned that I am improving my
diagnostic/error handling library to handle malloc() failures
predictably.)
 
S

Skarmander

jacob said:
but we are steering off topic and I fear the regulars coming :)

They're always there, and always watching us; hence, no need to be paranoid.

S.
 
I

Ian Collins

jacob said:
Keith Thompson a écrit :



Ahhh... You do not use debuggers very often?

Mmmm... Well, they are great tools. I use them very often,
actually I spend most of the time either in the editor
or in the debugger.
Try a different style of development. If you get used to working test
first, you will find yourself using the debugger less and less. You
write a failing test, then write the code to pass it. If everything
passes, move on. If something breaks, back out your change a try
something else.
 
S

Skarmander

Richard said:
Andrew Poelstra said:


Well, the check is done in the obvious way:

p = malloc(n * sizeof *p);
if(p == NULL)
{

The response to a failure depends on the situation. I've covered this in
some detail in my one-and-only contribution to "the literature",

I take it you're referring to "C Unleashed"; I haven't read it and am not
currently in a position to read it, so I hope you'll excuse me if I clumsily
raise points you discuss in depth in the book.
so I'll just bullet-point some possible responses here:

* abort the program. The "student solution" - suitable only for high school
students and, perhaps, example programs (with a big red warning flag).

This is not doing justice to the significant amount of work required to make
a program robust in the face of memory exhaustion. Quite bluntly: it's not
always worth it in terms of development time versus worst possible outcome
and likelihood of that outcome, even for programs used outside high school.

I'm not saying the tradeoffs involved are always correctly assessed (it's
probably a given that the cost of failure is usually underestimated), but I
do believe they exist.

I presume that instead of "aborting the program" we may read "exiting the
program immediately but as cleanly as possible", by the way, with the latter
being just that bit more desirable.
* break down the memory requirement into two or more sub-blocks.

Applicable only if the failure is a result of trying to allocate more memory
than you really need in one transaction, which is either a flaw or an
inappropriate optimization (or both, depending on your point of view).
* use less memory!

Will solve the problem, in the sense that "don't do that then" will cure any
pain you may experience while moving your arm. The bit we're interested in
is when you've decided that you absolutely have to move your arm.
* point to a fixed-length buffer instead (and remember not to free it!)

Thread unsafe (unavoidably breaches modularity by aliasing a global, if you
like it more general), increased potential for buffer overflow, requires
checking for an exceptional construct in the corresponding free() wrapper
(I'm assuming we'd wrap this).

I don't see how this would ever be preferential to your next solution:
* allocate an emergency reserve at the beginning of the program

How big of an emergency reserve, though? To make this work, your biggest
allocation should never exceed the emergency reserve (this could be tedious
to enforce, but is doable) and it will only allow you to complete whatever
you're doing right now.

Of all the solutions suggested, though, I'd say this one has the most
potential for practical success, *if* we signal the OOM condition *in
addition to* returning emergency reserve storage. At the end of an
individual transaction, we can detect that the next one will fail if no more
memory has become available, so we can take any measures we can take
(including failing gracefully) with the program in a well-known state (we
can do the same for a fail-fast allocation like malloc() offers, but it
requires more effort, and may involve rewriting things we can't rewrite).
* use virtual memory on another machine networked to this one (this
is a lot of work, but it may be worth it on super-huge projects)
This is not really an answer to "how do I deal with memory allocation
failure in a program", but to "how do I make sure my program doesn't
encounter memory allocation failure". For such projects as you mention
you'll probably want a custom memory allocation library to begin with (which
may or may not involve the standard malloc() at all, depending on the
portability/performance tradeoffs involved).
Well, I wasn't really trying to insult you. I was just trying to communicate
my concern at the prevalence of this really bad practice of not checking
that an external resource acquisition attempt succeeded. You wouldn't omit
a check on fopen or socket or connect, so why on malloc?
Playing devil's advocate for a moment: because the failures you cite are
both more common and easier to recover from, if they are recoverable at all,
so checking for them is more worthwhile.

This doesn't actually justify omitting a *check* on failure, of course,
since even dying with an error in a well-defined way is better than invoking
undefined behavior. The cost of checking is so insignificant that, if you
really need to get your savings there, you're probably doing memory
allocation wrong to begin with.

S.
 
R

Richard Heathfield

Skarmander said:
Richard Heathfield wrote:

I take it you're referring to "C Unleashed"; I haven't read it and am not
currently in a position to read it, so I hope you'll excuse me if I
clumsily raise points you discuss in depth in the book.


This is not doing justice to the significant amount of work required to
make a program robust in the face of memory exhaustion. Quite bluntly:
it's not always worth it in terms of development time versus worst
possible outcome and likelihood of that outcome, even for programs used
outside high school.

Perhaps I should have made it clearer that I'm referring to programs that
are intended to be used over and over and over by lotsa lotsa people. If
it's not worth the programmer's time to write the code robustly, it's not
worth my time to use his program. Of course, it may be worth /his/ time to
use his own program.
I'm not saying the tradeoffs involved are always correctly assessed (it's
probably a given that the cost of failure is usually underestimated), but
I do believe they exist.

Yes, the cost of failure can be a lost customer, repeated N times.
I presume that instead of "aborting the program" we may read "exiting the
program immediately but as cleanly as possible", by the way, with the
latter being just that bit more desirable.

You may indeed. I was using "abort" in a more general sense, not the
technical C sense (std lib function).
Applicable only if the failure is a result of trying to allocate more
memory than you really need in one transaction, which is either a flaw or
an inappropriate optimization (or both, depending on your point of view).

No, you've misunderstood. I'm thinking about situations where you would
ideally like a contiguous block of N bytes, but would be able to manage
with B blocks of (N + B - 1) / B bytes (or, more generally, a bunch of
blocks that between them total N bytes). The allocator might find that
easier to manage.
Will solve the problem, in the sense that "don't do that then" will cure
any pain you may experience while moving your arm. The bit we're
interested in is when you've decided that you absolutely have to move your
arm.

Consider reading a complete line into memory. It's good practice to extend
the buffer by a multiple (typically 1.5 or 2) whenever you run out of RAM.
So - let's say you've got a buffer of 8192 bytes, and you've filled it but
you still haven't finished reading the line. So you try to realloc to
16384, but realloc says no. Well, nothing in the rules says you're
necessarily going to need all that extra RAM, so it might be worth trying
to realloc to 8192 + 4096 = 12288 or something like that instead.
Thread unsafe (unavoidably breaches modularity by aliasing a global, if
you like it more general), increased potential for buffer overflow,
requires checking for an exceptional construct in the corresponding free()
wrapper (I'm assuming we'd wrap this).

Who says it has to be global? It would never have occurred to me to use a
file scope object for such a purpose.

I don't see how this would ever be preferential to your next solution:


How big of an emergency reserve, though? To make this work, your biggest
allocation should never exceed the emergency reserve (this could be
tedious to enforce, but is doable) and it will only allow you to complete
whatever you're doing right now.

Yes, but that might be enough to get the user's data to a point where it can
be saved, and reloaded later in a more memory-rich environment.

This is not really an answer to "how do I deal with memory allocation
failure in a program", but to "how do I make sure my program doesn't
encounter memory allocation failure".

Well, that would be nice, but I presume that most programmers would rather
have their RAM nice and local, where they can get at it quickly and easily.
It's a port in a storm, not a general purpose allocation strategy.

<snip>
 
K

Keith Thompson

Richard Heathfield said:
The response to a failure depends on the situation. I've covered this in
some detail in my one-and-only contribution to "the literature", so I'll
just bullet-point some possible responses here:

* abort the program. The "student solution" - suitable only for high school
students and, perhaps, example programs (with a big red warning flag).
* break down the memory requirement into two or more sub-blocks.
* use less memory!
* point to a fixed-length buffer instead (and remember not to free it!)
* allocate an emergency reserve at the beginning of the program
* use virtual memory on another machine networked to this one (this
is a lot of work, but it may be worth it on super-huge projects)
[...]

Out of curiosity, how often do real-world programs really do something
fancy in response to a malloc() failure?

The simplest solution, as you say, is to immediately abort the program
(which is far better than ignoring the error). The next simplest
solution is to do some cleanup (print a coherent error message, flush
buffers, close files, release resources, log the error, etc.) and
*then* abort the program.

I've seen suggestions that, if a malloc() call fails, the program can
fall back to an alternative algorithm that uses less memory. How
realistic is this? If there's an algorithm that uses less memory, why
not use it in the first place? (The obvious answer: because it's
slower.) Do programmers really go to the effort of implementing two
separate algorithms, one of which will be used only on a memory
failure (and will therefore not be tested as thoroughly as the primary
algorithm)?
 
R

Richard Heathfield

Keith Thompson said:

Out of curiosity, how often do real-world programs really do something
fancy in response to a malloc() failure?

Six times, for sufficiently variable values of six.
The simplest solution, as you say, is to immediately abort the program
(which is far better than ignoring the error). The next simplest
solution is to do some cleanup (print a coherent error message, flush
buffers, close files, release resources, log the error, etc.) and
*then* abort the program.

In other words, get out gracefully, preferably without losing any user data.
But that may involve completing the current task for which you wanted
memory in the first place.
I've seen suggestions that, if a malloc() call fails, the program can
fall back to an alternative algorithm that uses less memory. How
realistic is this?

I've had to do it myself in "real" code.
If there's an algorithm that uses less memory, why
not use it in the first place?

The answer is embarrassingly obvious.
(The obvious answer: because it's slower.)

Quite so.
Do programmers really go to the effort of implementing two
separate algorithms, one of which will be used only on a memory
failure

Yes, sometimes, if the situation warrants it. Often, it won't, but often !=
always. Normally, the goal is to fail gracefully without losing user data.
But some programs Must Not Fail - e.g. safety-critical stuff.
(and will therefore not be tested as thoroughly as the primary
algorithm)?

It needs to be tested thoroughly. The primary algorithm will be "tested"
more thoroughly in the sense that it's used more often and so there's a
greater likelihood that bugs will be exposed, but that's true of a lot of
code - for example, an OS's "copy a file" operation is tested more
thoroughly than its "format a disk drive" operation, simply by virtue of
the fact that users are more likely to /use/ the file copy, and every use
is a test. That doesn't mean the format code didn't get an appropriate
level of testing.
 
S

Skarmander

Richard said:
Skarmander said:


Perhaps I should have made it clearer that I'm referring to programs that
are intended to be used over and over and over by lotsa lotsa people. If
it's not worth the programmer's time to write the code robustly, it's not
worth my time to use his program. Of course, it may be worth /his/ time to
use his own program.
You're using "robust" as if it's a yes or no property. You're free to storm
off in a huff shouting "well that's not robust then!" if someone presents
you with a program that can deal with just about anything except memory
exhaustion when it finally comes round the bend, but that doesn't mean
everyone would or should.
Yes, the cost of failure can be a lost customer, repeated N times.
Don't sell it short. It could be a DEAD CUSTOMER, repeated N times.

Or it could be a single entry in a log somewhere.
No, you've misunderstood. I'm thinking about situations where you would
ideally like a contiguous block of N bytes, but would be able to manage
with B blocks of (N + B - 1) / B bytes (or, more generally, a bunch of
blocks that between them total N bytes). The allocator might find that
easier to manage.
I see what you're getting at; the allocator may be unable to satisfy your
request due to fragmentation and the like. This doesn't work very well as a
strategy to recover, however; you're looking at writing a program that
allocates either a contiguous region of bytes or (if that should happen not
to work) finds some way to deal with a bunch of regions.

If you need to be able to handle the latter, though, you'll write code that
deals with a bunch of regions in the first place, with the amount of regions
possibly equal to 1. (You may choose to split these cases, but it's unlikely
to become faster or more maintainable.)

Of course, then you need to settle on some sort of minimum region size
you're willing to accept, somewhere between the absolute lower bound of the
minimum allocator overhead and the minimum meaningful region for your
application.

All this will not make the program more *robust*, however, it'll simply
allow it to make better use of existing resources. ("Simply" should
nevertheless not be taken lightly.) Robustness is given by how well the
system behaves when the resources run dry, not how well it squeezes water
from the remaining stones when there's a drought in sight.
Consider reading a complete line into memory. It's good practice to extend
the buffer by a multiple (typically 1.5 or 2) whenever you run out of RAM.
So - let's say you've got a buffer of 8192 bytes, and you've filled it but
you still haven't finished reading the line. So you try to realloc to
16384, but realloc says no. Well, nothing in the rules says you're
necessarily going to need all that extra RAM, so it might be worth trying
to realloc to 8192 + 4096 = 12288 or something like that instead.
Thanks, that explanation was a bit more meaningful than "use less memory". :)

This is a good amendment to a standard exponential allocation strategy.
Who says it has to be global? It would never have occurred to me to use a
file scope object for such a purpose.
So N fixed-length buffers, then? Obviously N can't be dynamic, or you're
back to square one.
Yes, but that might be enough to get the user's data to a point where it can
be saved, and reloaded later in a more memory-rich environment.
Yes, as I said, of all your solutions, this one comes closest to actually
making things more robust in the face of imminent failure. The other items,
while helpful, delay the inevitable. Valuable as that is, I'm more
interested in what to do when the inevitable comes around, as it inevitably
will.
Well, that would be nice, but I presume that most programmers would rather
have their RAM nice and local, where they can get at it quickly and easily.
It's a port in a storm, not a general purpose allocation strategy.
A strange sort of port, when you need to first build a ship, then tow it
across the sea to its destination.

It's a "you might consider it" thing that seems very problem-specific to me;
"link computers together so you have more resources" is certainly an
approach, but not one I'd expect to see as a recommendation for making
programs more robust. That's not to say the suggestion itself isn't
valuable, of course.

S.
 
B

Ben Pfaff

Keith Thompson said:
Do programmers really go to the effort of implementing two
separate algorithms, one of which will be used only on a memory
failure (and will therefore not be tested as thoroughly as the
primary algorithm)?

Only occasionally, in my experience. Sometimes I do this if I
don't want to introduce an error path in a stack of function
calls that doesn't have one and which shouldn't have one.

One example is a merge sort (that requires O(n) extra storage)
that falls back to another sort algorithm if storage is not
available.
 
K

Keith Thompson

Richard Heathfield said:
Keith Thompson said: [...]
Do programmers really go to the effort of implementing two
separate algorithms, one of which will be used only on a memory
failure

Yes, sometimes, if the situation warrants it. Often, it won't, but often !=
always. Normally, the goal is to fail gracefully without losing user data.
But some programs Must Not Fail - e.g. safety-critical stuff.

In that context, I would think it would *usually* make more sense to
use just the slower and more robust algorithm in the first place.

Possibly the faster and more memory-intensive method would be
necessary to meet deadlines, and if you run out of memory you can fall
back to the slower method and continue running in a degraded mode.
But then again, safety-critical real-time code tends not to use
dynamic memory allocation at all.

I think that, 99% of the time, the best response to an allocation
failure is to clean up and abort. (The tendency to omit the "clean
up" portion is regrettable; the tendency to omit the "abort" portion,
i.e., to ignore the error, is even more so.) That other 1% requires
the other 99% of the effort.
 
A

Al Balmer

Richard Heathfield said:
Keith Thompson said: [...]
Do programmers really go to the effort of implementing two
separate algorithms, one of which will be used only on a memory
failure

Yes, sometimes, if the situation warrants it. Often, it won't, but often !=
always. Normally, the goal is to fail gracefully without losing user data.
But some programs Must Not Fail - e.g. safety-critical stuff.

In that context, I would think it would *usually* make more sense to
use just the slower and more robust algorithm in the first place.

Possibly the faster and more memory-intensive method would be
necessary to meet deadlines, and if you run out of memory you can fall
back to the slower method and continue running in a degraded mode.
But then again, safety-critical real-time code tends not to use
dynamic memory allocation at all.

I think that, 99% of the time, the best response to an allocation
failure is to clean up and abort. (The tendency to omit the "clean
up" portion is regrettable; the tendency to omit the "abort" portion,
i.e., to ignore the error, is even more so.) That other 1% requires
the other 99% of the effort.

I've worked on lots of safety-critical (and money-critical) systems,
and I'd emphasize the word "system." The program is only a part of the
system, albeit a very important part. Sometimes the best thing the
program can do is get out of the way and let the failsafes take over.
Sometimes, in fact, the computer itself gets out of the way, for
example if it detects an unrecoverable memory fault and can no longer
depend on it's own calculations.

This is a big subject, and 99% off-topic <G>.
 
S

Sjouke Burry

Skarmander said:
That's a common but by no means universal approach to coding.
Personally, I find that coding as if my system had no debuggers results
in code for which I don't need a debugger, and this is why I don't use
debuggers often.

I think that this style of coding is overall more speedy than the one
where you do assume debuggers are readily available for assistance, but
I have no hard data on it, and doubtlessly it will vary by individual.

Well, you could have used a different debugger... Or, like compilers
bootstrapping themselves, you could have used a rudimentary but easily
eye-proofed version of your debugger to develop the rest with.

S.
The only time I used a debugger in 35 years of
programming, was when an assembler decoding
subroutine failed to cross a 64K barrier properly.
(I did not write that one.).
To me, it smells like "let the system catch my errors"
while you should try to avoid them in the first place.
Sloppy typing can cause errors which are not
found by your compiler/debugger.
 
R

Richard Heathfield

Skarmander said:
Richard Heathfield wrote:
You're using "robust" as if it's a yes or no property. You're free to
storm off in a huff shouting "well that's not robust then!" if someone
presents you with a program that can deal with just about anything except
memory exhaustion when it finally comes round the bend, but that doesn't
mean everyone would or should.

<grin> No, there's no harumphing going on over here. But basic resource
acquisition checking is as fundamental to robustness as steering so as not
to bump the kerb is to driving.
Don't sell it short. It could be a DEAD CUSTOMER, repeated N times.

Absolutely. And a dead customer is a lost customer, right?
Or it could be a single entry in a log somewhere.

Or not even that. We're back to undefined behaviour.

I see what you're getting at; the allocator may be unable to satisfy your
request due to fragmentation and the like. This doesn't work very well as
a strategy to recover, however; you're looking at writing a program that
allocates either a contiguous region of bytes or (if that should happen
not to work) finds some way to deal with a bunch of regions.

Done it. It wasn't pretty, but it is certainly possible sometimes. Not all
the time, I grant you. (If there were one-size-fits-all, we'd all know
about it and probably many of us would be using it.)

All this will not make the program more *robust*, however, it'll simply
allow it to make better use of existing resources. ("Simply" should
nevertheless not be taken lightly.) Robustness is given by how well the
system behaves when the resources run dry, not how well it squeezes water
from the remaining stones when there's a drought in sight.

If the objective is to gather enough moisture to survive until the rescue
helicopter arrives - that is, if the objective is to complete the immediate
task so that a consistent set of user data can be saved before you bomb out
- then it's a sensible approach.

Thanks, that explanation was a bit more meaningful than "use less memory".
:)

I was trying not to rewrite the book, okay? :)
So N fixed-length buffers, then? Obviously N can't be dynamic, or you're
back to square one.

I was assuming just the one, actually, local to the function where the
storage is needed.

<snip>
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,755
Messages
2,569,536
Members
45,020
Latest member
GenesisGai

Latest Threads

Top