[net.lang.c] When does void make code less readable?

tut@sun.uucp (Bill Tuthill) (02/15/85)

In general, void is A Good Thing.  C routines that return a value
are like Pascal functions, while C routines not returning values
are like Pascal procedures, and should be declared as void to keep
things clear.  However, some system/library routines, such as close(),
fclose(), and free() return values that programs don't usually care
about.  If the system is unable to close a file, you've got problems
that a normal program can't deal with anyway.  Nonetheless, lint
encourages you to cast the return value of these routines to void.
Now, I invite people to prove that the simple

	fclose(fp);

is less portable than:

	(void)fclose(fp);

I consider the first more portable-- many compilers on cheap micros
don't know about void.  The first version is also more readable--
anytime you throw extra keywords in, legibility decreases.

Bill Tuthill

msb@lsuc.UUCP (Mark Brader) (02/17/85)

Consider the function strcpy.  The manual here declares this as:

     char *strcpy(s1, s2)
     char *s1, *s2;

The return value here is just a copy of the input value s1, provided
on the off-chance that it'll be useful to the calling program.  I claim
that all functions that don't otherwise return a value should strive
to return a might-be-useful value.  Then when you see a function call
whose result is unassigned, you know a return value is being ignored.
I think (void) should be reserved for really unusual cases, as documentation
that you really meant to do that.

I do support the use of lint, but I don't like (void).  It just clashes
with the language's style, to my mind.  After all, we can write:

	a = ++b;
or:
	++b;

but I don't want to write the latter as:

	(void) ++b;

everywhere.  What's the difference between this an a function call:

	increment(&b);
?

Mark Brader

ndiamond@watdaisy.UUCP (Norman Diamond) (02/18/85)

> I do support the use of lint, but I don't like (void).  It just clashes
> with the language's style, to my mind.  After all, we can write:
>          a = ++b;              or:               ++b;
> but I don't want to write the latter as:     (void) ++b;
> Mark Brader

Don't forget that you would also need:    (void) a = ++b;
because the = operator returns its value too!

(Except on one machine I used, where   a[1] = a[2] = 1.0;  worked fine but
   a[2*i+1] = a[2*i+2] = 1.0;  left a[2*i+1] unchanged.)
-- 

   Norman Diamond

UUCP:  {decvax|utzoo|ihnp4|allegra|clyde}!watmath!watdaisy!ndiamond
CSNET: ndiamond%watdaisy@waterloo.csnet
ARPA:  ndiamond%watdaisy%waterloo.csnet@csnet-relay.arpa

"Opinions are those of the keyboard, and do not reflect on me or higher-ups."

ksbszabo@wateng.UUCP (Kevin Szabo) (02/18/85)

In article <1995@sun.uucp> tut@sun.uucp (Bill Tuthill) writes:
 +--------
 |In general, void is A Good Thing.  C routines that return a value
 |are like Pascal functions, while C routines not returning values
 |are like Pascal procedures, and should be declared as void to keep
 |things clear.
 +--------
I thought that just using `return' instead of `return ( expression )'
told lint that the function wasn't returning a value, and hence
the returned value could be ignored. VOID tells lint that a
value is returned but it is ignored. Please correct me if I am wrong!
 +--------
 |However, some system/library routines, such as close(),
 |fclose(), and free() return values that programs don't usually care
 |about.  If the system is unable to close a file, you've got problems
 |that a normal program can't deal with anyway. 
 +--------
I agree. I think it would be a useful thing for routines that normally
"can't" fail, or return values that are ignored 90% of the time, to
provide instead a general `event' handling function. This could probably
look a little like the signal handling stuff. Thus we can have

	event( FAILED_PRINTF, bad_exit );
	event( FAILED_MALLOC, garbage_collect );

By default `events' could probably be a PERROR with an exit(1). This scheme
is not perfect but is much better than having to VOID every
system call that should always work. It is also a lot better
than segmentation faults/bus errors that occur when you try to carry
on after an unchecked system call does fail. Of course, it adds
complexity which is why it wasn't done this way in the first place.

After perusing the manual pages on our machine it looks like the Math
Faculty Computing Facility (at U of Waterloo) has already made a stab
at providing these event handling facilities for their new stuff.
I don't know anything about them though.

					Kevin
-- 
Kevin Szabo  watmath!wateng!ksbszabo (U of Waterloo VLSI Group, Waterloo Ont.)

jss@sjuvax.UUCP (J. Shapiro) (02/21/85)

[Pacman's revenge...]

	The real problem with the use of (void) is that too many current C
compilers don't support it, and don't know what the void cast should go to.
Many of the best of the microcomputer C compilers don't have it, and there
is an argument which runs: "C is small enough and simple enough to run on
micros, there is nothing intrinsically wrong with micros (NO FLAMES), and
backwards compatability is important."

	For all of my joking about a ^= b ^= a ^= b, the real reason not to add
things to a language is just this.  Why should I need to spend another $500
on a good compiler?  I am not that rich. (void) is therefore a THORN to
portability.  Even some UNIX lookalike compilers don't support it.

Jon Shapiro
Haverford College

UNIX is a trademark of big brother.

henry@utzoo.UUCP (Henry Spencer) (02/22/85)

>  |In general, void is A Good Thing.  C routines that return a value
>  |are like Pascal functions, while C routines not returning values
>  |are like Pascal procedures, and should be declared as void to keep
>  |things clear.
> I thought that just using `return' instead of `return ( expression )'
> told lint that the function wasn't returning a value, and hence
> the returned value could be ignored. VOID tells lint that a
> value is returned but it is ignored. Please correct me if I am wrong!

There are two different uses of void here:  using it to declare that
a function returns no value (to the compiler and to human beings, not
just to lint), and using it in a cast to throw away an unwanted return
value.  The former is presumably what Bill is referring to.

>  |However, some system/library routines, such as close(),
>  |fclose(), and free() return values that programs don't usually care
>  |about.  If the system is unable to close a file, you've got problems
>  |that a normal program can't deal with anyway. 

Actually, note that the asynchronous nature of Unix disk i/o can cause
a real, live i/o error to be reported to a close() rather than to the
write() that caused it.  Programs which really want to be paranoid *will*
check the returned value from close() and fclose().

The basic problem here is that most programs want to see an error-free
i/o system abstraction, but a few want to know about problems.  Splitting
these two levels would simplify this problem immensely, but it's a bit
late for that now.

> I agree. I think it would be a useful thing for routines that normally
> "can't" fail, or return values that are ignored 90% of the time, to
> provide instead a general `event' handling function...

I am deeply suspicious of event-handling primitives; I don't think I
have ever seen a good way of doing them.  Lots of bad ways, though.
One thing which *can* be very handy is variants of the standard routines
which are "guaranteed" to work because they abort the program if the
standard routine underneath returned an error.  The K&P efopen() is a
prime example.  We also now have an emalloc(), which is an enormous
convenience since there is very seldom anything useful to do when malloc
fails.  (Incidentally, a global s/malloc/emalloc/ is an excellent way
of improving the robustness of 4.2BSD code.)  Although this sort of
thing can't be incorporated into old code just by recompiling, in new
code it's much simpler than introducing pseudo-signals and hence
asynchronism and all the nightmares that involves.
-- 
				Henry Spencer @ U of Toronto Zoology
				{allegra,ihnp4,linus,decvax}!utzoo!henry

g-frank@gumby.UUCP (02/22/85)

> 	The real problem with the use of (void) is that too many current C
> compilers don't support it, and don't know what the void cast should go to.

   Try 
	typedef void unsigned ;

   This is what Whitesmith's did all through the Idris system for the
68000, and it seems to work.  Obviously, if your compiler (and lint)
don't recognize it, it won't buy you any immediate benefit, but if you
go to port your code to an implementation that does use it, your code
will be "ready for prime time," as it were.


-- 
      Dan Frank

	  Q: What's the difference between an Apple MacIntosh
	     and an Etch-A-Sketch?

	  A: You don't have to shake the Mac to clear the screen.

gwyn@brl-tgr.ARPA (Doug Gwyn <gwyn>) (02/23/85)

> 	typedef void unsigned ;

Nope.  Try
	typedef int	void;
or
	#define void	int
(Use "unsigned" if you wish; it shouldn't matter.)

gwyn@brl-tgr.ARPA (Doug Gwyn <gwyn>) (02/25/85)

> One thing which *can* be very handy is variants of the standard routines
> which are "guaranteed" to work because they abort the program if the
> standard routine underneath returned an error.

I much prefer to have a low-level routine return an indication to its
invoker when it cannot do its function for whatever reason.  Strategy
should be managed at higher levels.

henry@utzoo.UUCP (Henry Spencer) (03/03/85)

> > One thing which *can* be very handy is variants of the standard routines
> > which are "guaranteed" to work because they abort the program if the
> > standard routine underneath returned an error.
> 
> I much prefer to have a low-level routine return an indication to its
> invoker when it cannot do its function for whatever reason.  Strategy
> should be managed at higher levels.

You've missed my whole point, Doug.  The low-level routines are not
pre-empting the decision on how to handle errors, they are aiding in
the implementation of the most common decision:  "on error, print a
message and die".  By calling (say) emalloc rather than malloc, the
higher levels are signifying their decision to adopt this strategy,
and are asking the lower levels to handle the implementation.  There
is no difference in power or flexibility, only in ease of use.  As
somebody (John Mashey?) once said:

	If you want people to follow a standard, it must be
	easier to be standard than to be non-standard.

Note my earlier comment about the usefulness of a global s/malloc/emalloc/
in Berkeley code.  By requiring the caller to do the work of checking
for success, even when there is nothing meaningful to be done about
failure, the bare malloc interface encourages sloppy programmers to
ignore the whole issue.  It also makes conscientious programmers do
repetitive and annoying extra work.

Of course, we need the bare interface:  sometimes there *is* meaningful
action to be taken in the event of failure.  But usually, the only
thing to do is complain and die.  It makes all kinds of sense to package
this common case in a form that makes it easy to use, so that people
will do it right.  It also makes a remarkable difference in ease of use
if a program is doing mallocs all over the place; the difference is
marked enough to inspire the "why didn't we put that in the library
long ago?" feeling that marks a good library routine.  Try it, you'll
like it.
-- 
				Henry Spencer @ U of Toronto Zoology
				{allegra,ihnp4,linus,decvax}!utzoo!henry

gwyn@brl-tgr.ARPA (Doug Gwyn <gwyn>) (03/06/85)

Re: emalloc() etc.

I also often package malloc() in routines like NodeAlloc() etc. that
alloc the proper thing (e.g. a (struct node)) and perform the appropriate
action when the malloc() fails.  However, what the right action in this
case really is, is dependent on the application.  In production code, it
is seldom appropriate to just print an error message and terminate the
process.  I built an interactive drawing system once that had to keep
running in case of ANY foreseeable emergency, in order not to lose the
user's hours of work when there was a system failure.  In case of running
out of memory, it set a flag which would cause display of an "out of
memory" message at the appropriate time, and returned failure status from
the allocator to its invoker so something sensible could be done to
maintain the integrity of the linked data structures.  Note that there
was no "stderr" to print messages to (I had to arrange for messages to
appear in the menu area of the refresh display list), and the message had
to be deferred until the whole data structure had been processed, to
avoid a cascade of error messages when one would do.

I am not saying that such thorough error handling is ALWAYS required,
but I would encourage program designers to think carefully about the
handling of exceptional conditions wherever they may occur.  I fear
that providing an emalloc() in the standard library would mainly make
it easier for programmers to excuse their not thinking about what to do
in the error case.  It wouldn't be any worse than not checking for
malloc() failure, though.  What we really need to do is to figure out
some way to stamp out sloppy programming.  Any ideas?