"expressions", not "statements", but I think this is probably true.
You similarly don't tend to see many addition operators where their value
isn't used. In general, operators are used only when we want their
values, except when they have side effects.
Exactly. The same is not however true for if/else statements. It is quite
reasonable to see an if/else statement where no value is assigned that
persists outside of the if/else block.
if (condition)
/* enable bus */
else
/* print error */
That's not really "overloading".
No, I didn't have a more exact term to describe it, and since C does not
allow overloading, I thought that it might be descriptive in this case.
The fact is that you are using p in two different ways to send two
different kinds of information. This isn't really all that different from
the ways many standard functions like fgetc() return an int for what is
expected to be a char value so that they can also send error information
along with any values. This is questionable practice that tends to cause
lots of trouble (in this case particularly to newcomers.)
In general, it is a much better practice to explicitly separate the data
outputs. The structure is sent in channel while the error code is sent
separately out of channel. If I was going to write a function that creates
p, I often use something like this:
typedef enum fulfillment_type {SUCCESS, FAILURE} fulfillment;
/*
creates a valid printer_dev at the buffer pointed to by printer -- returns
SUCCESS or FAILURE depending on whether a valid pring_dev could actually be
created
*/
fulfillment create_printer(printer_dev* printer);
In this way, I have an explicitly differentiated the error information from
the data itself.
Furthermore, I have allowed the possibility of later expanding the
interface to include values other then success or failure which may help
with error handling down the road. As somebody pointed out, there are
different reasons why the printer may not be available. NULL only
allows you to say that it isn't available; but, provides no information as
to why. It could be that the printer is not physically available or that
there are printers available but the user has not choosen which to use for
this operation. By using an external error indicator, I might have my
error handing code call a printer select dialog if printers are available
but none have been chosen.
That seems much, much, harder to arrange.
Not really. There may be several layers on top of the extra printer
handling functionality. In this example, one would expect to see code
which confirms that there is valid data to print, converts the data into a
format that the printer understands, then it actually calls the printer
handling code to send data to the printer. If the code checks for a valid
printer when a printing operation is requested, then non of this needs to
be called.
No, it's only a requirement for all the places at which you could enter
the code. If the only interface provided is
do_print(printer *p, struct print_me *data);
then only do_print has to handle this.
Agreed, but the code that calls do_printer still must be able to handle the
differing ways in which do_printer might fail.
Yes, but the way you handle it is by storing a null pointer to indicate
that there isn't anything to point to.
As I have already said, a NULL pointer indicates that there is an error.
It doesn't explain why. Even with an absence of requirements that different
errors handled differently, it is much more descriptive to say:
if (is_printer_available())
/* ... */
else
/* error code */
then:
if (printer != NULL)
/* ... */
else
/* error code */
The second only tells me that there is a problem. The second tells me that
there is an error because no printer is available.
That seems a LOT more difficult. You then have to have every single place
that might want to ask that someone do something involving a printer check
that, which is more work.
Which is easier?
Case #1:
int do_print(printer *p, struct print_me *data) {
if (!p)
return -1;
/* code using p */
}
...
do_print(default_printer, file_data);
...
do_print(alternate_printer, file_data);
...
do_print(color_printer, stored_entries);
Case #2:
int do_print(printer *p, struct print_me *data) {
/* code using p */
}
...
if (default_printer)
do_print(default_printer, file_data);
...
if (alternate_printer)
do_print(alternate_printer, file_data);
...
if (color_printer)
do_print(color_printer, stored_entries);
You mistaken in my reasoning. Likely, the test will be performed much
higher up before the system even attempts to generate information to send
to the printer. It makes no sense to do this much if there is not going to
be an printer to send it to. Secondly, the test will likely only need to
be in a single place inside of the function interface that actually
initiates any printer operations.
I think the former makes more sense, and is also more robust.
First see above, since test only needs to be called at the first common
entry into the printing system it is no less robust.
Second, while allowing an error to propogate through the code, there are
many more places that it could fail. Allowing a NULL printer value not
only creates problems in the actual printer handling code but could cause
issues where the program has to decide what output to send to the printer
etc.
In my model, it is illegal to call print handling code if the printer is
not valid in the first place. This would be enforced by assertions to make
sure the code is never used in this way. Since the code is never even
called for an invalid printer, there is no way that any piece of code which
was not properly designed to handle the missing printer could cause any
issues. I call this more robust.
That it might be, but it is easy to imagine a case where the
simplest thing to do is to merge the cases since "no printer exists"
is clearly very much like errors a printer could have.
The simplest thing to do isn't always the best thing to do.
It might be, but if the handling for error conditions is reasonably elaborate,
being able to route both cases through a single hunk of code may be easier
to maintain. Minimize special cases early and often!
Congratulations, this is the first decent argument that I have seen used in
this sub-thread. I do however disagree that this is a problem.
1. I would not even enounter this problem because I have made sure this
incident never arises as in my model, this code never would have
been called.
2. I don't think this really adds any significant complication and omits
the "special case" far earlier so that the same bit of code is not
complicated in having to use a value in two different ways.