S
Seebs
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.
Hmm.
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.)
Oh, certainly it's questionable. Go ahead and question it.
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:
I am not sure this is actually that much better a practice, especially in C,
where it's pretty much idiomatic to use a null pointer to indicate "we don't
have a foo".
/*
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.
And also forced the user to know how to allocate a printer, which does nothing
but push the issue somewhere else. Now the user has to write:
printer_dev *p = get_new_printer();
if (p)...
So you're right back where you started.
You can, of course, do:
fulfillment create_printer(printer_dev **printer);
and declare that, if it doesn't return SUCCESS, it hasn't done anything to
"printer".
But that's annoying! Furthermore, even if you've done this, you STILL have
the possibility that "printer" will be a null pointer.
You can never eliminate the possibility of a pointer being null-or-invalid
unless it's the address of a static object. For most real-world cases,
dynamic allocation is necessary. You can't detect an invalid pointer, so
you have to use null as the "we don't have one" state.
Since you MUST do that, it is ridiculous to then attach a second status that
isn't the pointer's status, because the moment you do that, you've created
two pieces of information which are supposed to reflect the same thing, and
thus the possibility for them to be out of sync.
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.
Right, because at the point where you'd be *using* the printer, you don't
care why. It's only at the point that an attempt was made to *allocate*
a printer that you care why, and the error handling for that belongs
there.
Not really. There may be several layers on top of the extra printer
handling functionality.
That would be "much, much, harder to arrange". You're creating bloat in
code, data storage, and layers of abstraction, all to avoid something that
is clear, idiomatic, and useful.
Agreed, but the code that calls do_printer still must be able to handle the
differing ways in which do_printer might fail.
Yup. But "no printer available" is plenty complete for this point; the
point at which a more specific problem with printer availability would
have been addressed would have been when p was configured.
As I have already said, a NULL pointer indicates that there is an error.
It doesn't explain why.
It doesn't have to. The "why" was addressed when the pointer failed to
acquire a non-null value. Past that, all you need to know is that there
is not currently a printer.
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.
They're completely identical. Both tell you that there is no printer
available. If there were a printer available, printer would not be a null
pointer.
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.
This seems very speculative, and you've repeatedly talked about adding
additional layers of interfaces, which sounds like a lot of overhead to
satisfy a prejudice.
In C, a null pointer is idiomatically used to indicate the unavailability
of a thing that would otherwise have been pointed to. That's the idiom
the language standard uses, for everything from malloc() to fopen(). Using
any other idiom is gratuitously confusing.
First see above, since test only needs to be called at the first common
entry into the printing system it is no less robust.
I don't buy this notion of a "first common entry into the printing system".
The printing system is used by the rest of the program; every part of the
rest of the program that might wish to print has to know how to tell what
it can or can't do.
Second, while allowing an error to propogate through the code, there are
many more places that it could fail.
Not really. It's just as practical to have the "first common entry" treat
a null pointer as evidence that there's no printer as to have some other
elaborate scheme.
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.
It's *at worst* no different at all from yours. There are many easily
constructed cases where it's better.
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.
This is too vague and hand-wavey to constitute an argument or a design
description. Present a complete, self-contained, example program showing
how you think this would work. I'm not buying it at all; so far, it sounds
like you're inventing a huge amount of overhead in order to synchronize
two pieces of information, and thus introducing an entire new *class* of
errors that are impossible with the standard idiom.
The simplest thing to do isn't always the best thing to do.
Not always, but until you have an explanation of something better which
is both concrete and coherent, and actually shows some kind of evidence
for why it's better other than your dislike of the completely standard
C idiom, which everyone is already used to and using and which works reliably
in billions of lines of code, I'm going to do something simple and reliable.
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.
You're just pushing the problem out a layer of abstraction without changing
it.
SOME code is called. Doesn't matter whether it's this code or your
completely gratuitous and probably buggy abstraction layer that you invented
to work around the standard C idiom. Some code is called, and must deal
with the possibility that no printer is available.
I'm using your printer layer. I want to print something. I must deal with
the possibility that there is no printer available. It doesn't matter
whether I deal with this by checking whether a pointer is null or doing
something else; I still have to deal with it.
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.
If sentinel values were some kind of incredibly arcane feature that no one
had ever heard of and that an ordinary C programmer might never have
encountered, this would be an argument.
They're not. It isn't.
There is no way to get away from the existence of null pointers, and the
possibility that a given pointer might be null when it's handed to your
code. You have to write the code for that anyway. By long-standing idiom,
that's the way to indicate "and we don't have one of these".
My solution:
void error_out(char *msg) {
fprintf(stderr, "fatal error: %s\n", msg ? msg : "unknown error");
exit(1);
}
...
error_out("couldn't open a file, sorry.");
...
error_out(NULL);
Your solution:
typedef enum error_message_availability {
ERROR_NOT_AVAILABLE,
ERROR_AVAILABLE,
} error_message_availability_t;
void error_out(error_message_availability_t avail, char *msg) {
if (avail == ERROR_AVAILABLE)
fprintf(stderr, "fatal error: %s\n", msg);
else
fprintf(stderr, "fatal error: unknown error\n");
exit(1);
}
...
error_out(ERROR_AVAILABLE, "couldn't open a file, sorry.");
...
error_out(ERROR_NOT_AVAILABLE, /* what goes here? */);
In your system, it is now possible to write:
error_out(ERROR_AVAILABLE, NULL);
and cause a null pointer dereference. In mine, it's not.
Furthermore, you still haven't suggested what should go in the "msg" slot
if there is no error. Or do you want us to do:
void error_out_with_message(char *msg) {
fprintf(stderr, "fatal error: %s\n", msg);
exit(1);
}
void error_out_without_message(void) {
fprintf(stderr, "fatal error: unknown error\n");
exit(1);
}
...
if (error_availability == ERROR_AVAILABLE)
error_out_with_message("couldn't open a file, sorry.");
else
error_out_without_message();
This is just ridiculous. You still have to deal with the fact that someone
can pass a pointer to your code, and that pointer could be a null pointer.
You can never escape that in any program which uses dynamic allocation. If
you allow users to specify the thing they want you to operate on by passing
a pointer to it, the pointer can be null. If you don't, you have a crippled
interface and people won't be able to make much use of it.
-s