What's The Best Practice Defining Error Codes in C

R

rammy

Hi,

I would like to have someone comments on what's the best practice
defining error codes in C.
Here's what I think:
solution A:
using enum
pros: type safe. better for debug (some debugger will show the name
not only the value)
cons: enum can not be forward declared which makes all error codes
couples together with the error code type ( enum )

Solution B:
using #define
pros: decouple, error codes could be defined in different .h file
cons: macro is bad. no type safe

Solution C:

typedef struct Error
{
int value;

} Error;

static Error const ERROR_OUT_OF_SPACE = { 123 };

pros: type safe. decouple, the error code type is no longer bound with
all the error definition
cons: I don't know any one doing it this way so I'm not sure if it has
some drawbacks or is it bad for (runtime/space) performance.
If using pure C, user could not compare the error value directly but
have to compare the inner "value" member which is not convenience.

Thanks for your help :)
 
S

Stefan Ram

rammy said:
using #define
pros: decouple, error codes could be defined in different .h file
cons: macro is bad. no type safe

Most libraries I am aware of, use #define IIRC. So, I'd use that, too.

But there are more decisions to be made:

Do you want to have codes be unique per function, per library, or
globally (as far as code was written under your control)?

Do you want codes to be structured (so that certain bits have certain
meanings) or unstructured?

Heck, sometimes, I just use naked literals: 1, 2, 3, and so ... .
 
E

Eric Sosman

Hi,

I would like to have someone comments on what's the best practice
defining error codes in C.
Here's what I think:
solution A:
using enum
pros: type safe. better for debug (some debugger will show the name
not only the value)
cons: enum can not be forward declared which makes all error codes
couples together with the error code type ( enum )

"Type safe" is too optimistic. An enum is some kind of integer
type, and integer types are freely interconvertible (as long as the
values are in range). A named enum value is just an `int' constant,
and can be stored in any integer variable (of sufficient range).
Try it yourself:

enum Fruit { APPLE, BANANA, CHERRY } fruit;
enum Motor { OTTO, DIESEL, STEAM } motor;
fruit = OTTO;
motor = BANANA;
fruit = PEAR * STEAM;
motor = sqrt(42.0);
printf("%g\n", cos(motor));
...
Solution B:
using #define
pros: decouple, error codes could be defined in different .h file
cons: macro is bad. no type safe

"Macro is bad" is -- well, that statement is bad.
Solution C:

typedef struct Error
{
int value;

} Error;

static Error const ERROR_OUT_OF_SPACE = { 123 };

Aside: Are you aware that ERROR_OUT_OF_SPACE is a reserved
name whenever said:
pros: type safe. decouple, the error code type is no longer bound with
all the error definition
cons: I don't know any one doing it this way so I'm not sure if it has
some drawbacks or is it bad for (runtime/space) performance.
If using pure C, user could not compare the error value directly but
have to compare the inner "value" member which is not convenience.

This is type safe, but in a limited way. If the struct
declaration is visible (as it would have to be to allow access
to the embedded member), there's nothing to prevent someone
creating and returning an Error value you've never heard of:

Error do_something(void) {
...
Error x;
x.value = rho * sin(theta);
return x;
}

.... could inject an "unauthorized" code into your system. This
could be either a pro or a con, depending on your point of view.

You could regain some type safety by declaring the struct as
an incomplete type in the public header, declaring some instances
of it, and using pointers to those instances as the error codes:

// In the header:
typedef struct Error *Error;
extern struct Error OUT_OF_SPACE;
extern struct Error OUT_OF_SIGHT;
extern struct Error OUT_OF_MIND;
...

// Usage:
Error do_something(void) {
...
return &OUT_OF_MIND;
}
Since `struct Error' is incomplete, no one can create a new
instance to point at and thereby give rise to unknown codes.
Of course, even a moderately determined antagonist can still
cause trouble with a cast:

Error be_troublesome(void) {
return (Error)42;
}

To the wider question, I don't think there's a "One size fits
all" solution. Sometimes you need only a success/failure report,
and this sort of machinery is overkill. Sometimes success and
failure come in several kinds (non-overlapping, not too many), and
schemes of the sort you describe may be appropriate. Sometimes you
need to report several things about a failure or success (not just
"login failed" but "login failed because authentication server did
not respond because no connection to authentication server because
a network cable is unplugged"), and a single-code-per-failure-mode
approach would suffer combinatorial explosion.

Instead of seeking a single "best" error-reporting scheme (which
I doubt exists), I suggest you contemplate the library or other
facility that you're building, and ask yourself what kinds and amounts
of status information the callers can make good use of. You're likely
to come up with different scenarios for different facilities, so pick
a facility-specific approach -- and implement it well.
 
M

Malcolm McLean

בת×ריך ×™×•× ×©×™×©×™, 29 ביוני 2012 21:29:33 UTC+1, מ×ת rammy:
I would like to have someone comments on what's the best practice
defining error codes in C.
The way I do it is this. I divide the soruce files for the program into four groups: this program only, this platform only. this program only, any platform, this platform only, any program. Any program, any platform.
So a random number generator would most likely be "any program,any platform". Code to make the spaceship fire when the player presses the spacebar would probably be this platform only, this program only.
For the last two groups, the "any program" files, you need to pass up errors to the caller. Let's say that someone tries to generate a random number with a range too big for our generator to handle. We might decide that in this case, we'll always return -1. The process is ad hoc, and there shouldn'tbe any dependency. We need to be able to cut and paste our random number generator from the space invaders game, put it into a roulette game or a protein structure predictor, and have it still work.
For the first two groups, the files will never be used outside of the specific project. So it might be appropriate to have centralised errors with a file "errorcodes.h" somewhere containing a list of human-meaningful enums.
 
R

rammy

"Type safe" is too optimistic. An enum is some kind of integer
type, and integer types are freely interconvertible (as long as the
values are in range). A named enum value is just an `int' constant, and
can be stored in any integer variable (of sufficient range). Try it
yourself:

enum Fruit { APPLE, BANANA, CHERRY } fruit; enum Motor { OTTO, DIESEL,
STEAM } motor; fruit = OTTO;
motor = BANANA;
fruit = PEAR * STEAM;
motor = sqrt(42.0);
printf("%g\n", cos(motor));
...

Yes and No. As a module We'd like to split the error code to two
category, those for the client and those inside. We don't want to
define all the error code in one place which introduce unnecessary
couple. But for one category, it's better that they are defined in one
place.
"Macro is bad" is -- well, that statement is bad.

So I guess you dislike macro either :)
Aside: Are you aware that ERROR_OUT_OF_SPACE is a reserved
name whenever <errno.h> is included?


Thanks for the reminder. Actually all identifiers are prefixed with
module name.(how long will namespace be introduced into C)
This is type safe, but in a limited way. If the struct
declaration is visible (as it would have to be to allow access to the
embedded member), there's nothing to prevent someone creating and
returning an Error value you've never heard of:

Error do_something(void) {
...
Error x;
x.value = rho * sin(theta);
return x;
}

... could inject an "unauthorized" code into your system. This could be
either a pro or a con, depending on your point of view.

You could regain some type safety by declaring the struct as
an incomplete type in the public header, declaring some instances of it,
and using pointers to those instances as the error codes:

// In the header:
typedef struct Error *Error;
extern struct Error OUT_OF_SPACE;
extern struct Error OUT_OF_SIGHT;
extern struct Error OUT_OF_MIND;
...

// Usage:
Error do_something(void) {
...
return &OUT_OF_MIND;
}
Since `struct Error' is incomplete, no one can create a new instance to
point at and thereby give rise to unknown codes. Of course, even a
moderately determined antagonist can still cause trouble with a cast:

Error be_troublesome(void) {
return (Error)42;
}

By decouple I mean the possibility to define new error code
somewhere else. I know it requires discipline to do so. Like I said
we'd like to separate private error code from public interface.
To the wider question, I don't think there's a "One size fits
all" solution. Sometimes you need only a success/failure report, and
this sort of machinery is overkill. Sometimes success and failure come
in several kinds (non-overlapping, not too many), and schemes of the
sort you describe may be appropriate. Sometimes you need to report
several things about a failure or success (not just "login failed" but
"login failed because authentication server did not respond because no
connection to authentication server because a network cable is
unplugged"), and a single-code-per-failure-mode approach would suffer
combinatorial explosion.

Instead of seeking a single "best" error-reporting scheme (which
I doubt exists), I suggest you contemplate the library or other facility
that you're building, and ask yourself what kinds and amounts of status
information the callers can make good use of. You're likely to come up
with different scenarios for different facilities, so pick a
facility-specific approach -- and implement it well.

I will stick with solution A) for now.

However, another silly question:

How about:

typedef struct ErrorTag* Error;
static Error const = 123;

I'm living in C++ world, so I want to try my best to make the API type
safe. Is there any way except enum could give a type safe error code
solution ?

Thanks for your time :)
 
B

Ben Bacarisse

rammy said:
I'm living in C++ world, so I want to try my best to make the API type
safe. Is there any way except enum could give a type safe error code
solution ?

What does "living in C++ world" mean? Do you mean you are used to C++'s
more sophisticated error reporting facilities, or it slightly stronger
type checking?

C is not a type-safe language (neither is C++ for that matter) so unless
you say what you are prepared to compromise on there is no solution at
all.
 
I

Ian Collins

On 07/ 1/12 08:57 AM, rammy wrote:

By decouple I mean the possibility to define new error code
somewhere else. I know it requires discipline to do so. Like I said
we'd like to separate private error code from public interface.

A couple of projects I have worked on solved this by defining the error
codes in something other than C and generating the C header files from
there. So each set got its own header, but the overall definitions were
elsewhere. This had the bonus advantage of providing the "extensible
enum" support C lacks.
I'm living in C++ world, so I want to try my best to make the API type
safe. Is there any way except enum could give a type safe error code
solution ?

You will have to accept that from a C++ programmer's perspective, enums
in C are somewhat broken due to automatic conversions. While C++ rules
prevent silly mistakes like

typedef enum { Good, Bad } Result;

Result oops() { return -1; }

C unfortunately does not. So from a type safety perspective, enums are
no better than #defines.
 
E

Eric Sosman

Yes and No. As a module We'd like to split the error code to two
category, those for the client and those inside. We don't want to
define all the error code in one place which introduce unnecessary
couple. But for one category, it's better that they are defined in one
place.

Perhaps my typo caused you to miss the point :-( What I mean
is that an enum is not "type safe" as you seem to think. C will
not prevent you from setting an `enum X' variable to an `enum Y'
value, nor to `42', nor to the result of some arbitrary expression.

enum X { A, B, C } foobar(void);
switch (foobar()) {
case A: ... break;
case B: ... break;
case C: ... break;
default: ... // Yes, this case *can* occur.
}
So I guess you dislike macro either :)

Macros are a part of the C language. They are sometimes useful,
sometimes dangerous -- like most of the rest of C. The blanket
statement "macro is bad" is nonsense.
Thanks for the reminder. Actually all identifiers are prefixed with
module name.(how long will namespace be introduced into C)

Probably never, although no deity has granted me the gift of
prophecy and I could be mistaken. If namespaces ever *are* added
to C, their use will be "mostly optional" in the name of backwards
compatibility: The investment in existing non-namespace C code is
enormous, and cannot simply be abandoned.
[...]
By decouple I mean the possibility to define new error code
somewhere else. I know it requires discipline to do so. Like I said
we'd like to separate private error code from public interface.

Extensible error codes (extensible codes of any kind) require
discipline from both the producer and the consumer. For example,
if a caller uses foobar() and checks for SUCCESS or OUT_OF_SPACE,
a future foobar() version that starts reporting QUOTA_EXCEEDED as
well may break the caller. Silently. So: "Be careful out there."
However, another silly question:

How about:

typedef struct ErrorTag* Error;
static Error const = 123;

The compiler is required to issue a diagnostic, for at least
two reasons: First, the second line looks like a declaration but
declares no identifier. Second, the constant `123' cannot be
converted automatically to any kind of pointer. I cannot tell
what you intended to write, so I can't comment on "How about."
I'm living in C++ world, [...]

Then you're in the wrong newsgroup. comp.lang.c++ is just
down the hall to your right, past the broom closet.
 
K

Keith Thompson

Eric Sosman said:
On 6/30/2012 4:57 PM, rammy wrote: [...]
I'm living in C++ world, [...]

Then you're in the wrong newsgroup. comp.lang.c++ is just
down the hall to your right, past the broom closet.

I thought it was past the water cooler.
 
E

Eric Sosman

Eric Sosman said:
On 6/30/2012 4:57 PM, rammy wrote: [...]
I'm living in C++ world, [...]

Then you're in the wrong newsgroup. comp.lang.c++ is just
down the hall to your right, past the broom closet.

I thought it was past the water cooler.

You're right. Gosh, that thing sure needs fixing-up!
Ah, but that's C++ for you: All structure, no maintenance.
 
M

Maxim Fomin

воÑкреÑенье, 1 Ð¸ÑŽÐ»Ñ 2012 г., 1:34:07 UTC+4 пользователь Ian Collins напиÑал:
On 07/ 1/12 08:57 AM, rammy wrote:



You will have to accept that from a C++ programmer's perspective, enums
in C are somewhat broken due to automatic conversions. While C++ rules
prevent silly mistakes like

typedef enum { Good, Bad } Result;

Result oops() { return -1; }

C unfortunately does not. So from a type safety perspective, enums are
no better than #defines.

From what and to what are enumerations in C converted?
 
I

Ian Collins

воÑкреÑенье, 1 Ð¸ÑŽÐ»Ñ 2012 г., 1:34:07 UTC+4 пользователь Ian Collins напиÑал:

From what and to what are enumerations in C converted?

Anything that can be converted to or from an int, which includes other
enums.
 
M

Maxim Fomin

воÑкреÑенье, 1 Ð¸ÑŽÐ»Ñ 2012 г., 11:05:22 UTC+4 пользователь Ian Collins напиÑал:
Anything that can be converted to or from an int, which includes other
enums.

AFAIK enumeration constants are of type int, so they are not converted to int. Example above is closely equivalent (but not 100%) to:

#define Good 0
#define Bad 1
typedef ???? Result
.....
Result oops() { return -1; }
where ???? is for char, int or unsigned int
It is obvious that -1 may or may be not within intended return range. However, nothing stops from:
int oops(); // should return only withing [100; 1000]
....
if (some error)
return -1; // forgot about spec
....

Theoretically problem may occur when ???? is char and oops() returns value out of char range or ???? is unsigned int an negative value is returned. Inboth cases compiler would issue warning about implicit conversions.

IMHO the problem is not that C enum is "type unsafe" but is in applying identical assumptions to different things.
 
B

Ben Bacarisse

Maxim Fomin said:
воÑкреÑенье, 1 Ð¸ÑŽÐ»Ñ 2012 г., 11:05:22 UTC+4 пользователь Ian Collins напиÑал:
AFAIK enumeration constants are of type int, so they are not converted
to int.

Agreed, but your question was about enumerations and not just their
constants so you need to consider what happens to the values of enum
objects as well as to enum constants.
Example above is closely equivalent (but not 100%) to:

#define Good 0
#define Bad 1
typedef ???? Result
....
Result oops() { return -1; }
where ???? is for char, int or unsigned int
It is obvious that -1 may or may be not within intended return range.

Absolutely (though the type ???? could be any signed or unsigned integer
type). It is even possible to get an integer overflow: had the return
been "return 999;" on a system where the compiler chose an 8-bit signed
char for the enum.
However, nothing stops from:
int oops(); // should return only withing [100; 1000]
...
if (some error)
return -1; // forgot about spec
...

Showing something else that C can't express is not a very strong
argument for C's enums being how they are! But maybe you were just
saying that the enum behaviour fits with the fact that C does not have
range types.
Theoretically problem may occur when ???? is char and oops() returns
value out of char range or ???? is unsigned int an negative value is
returned. In both cases compiler would issue warning about implicit
conversions.

Well it might, but it doesn't have to. I don't think gcc does, though
there may be an option I'm missing that makes it warn about such things.
IMHO the problem is not that C enum is "type unsafe" but is in
applying identical assumptions to different things.

I don't know what you mean here. C's enums present the programmer with
a number of things to wary of; one of the biggest being that an enum
type is simply a new type compatible with some unknown integer type.
That's not always what people expect so, like so much of C, you really
have to know it not to get tripped up.
 
T

Tim Rentsch

Ben Bacarisse said:
Example above is closely equivalent (but not 100%) to:

#define Good 0
#define Bad 1
typedef ???? Result
....
Result oops() { return -1; }
where ???? is for char, int or unsigned int
It is obvious that -1 may or may be not within intended return range.

Absolutely (though the type ???? could be any signed or unsigned integer
type). It is even possible to get an integer overflow: had the return
been "return 999;" on a system where the compiler chose an 8-bit signed
char for the enum. [snip unrelated]

Strictly speaking that isn't an overflow, but an
(integer) out-of-range conversion.
 

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

No members online now.

Forum statistics

Threads
473,755
Messages
2,569,537
Members
45,022
Latest member
MaybelleMa

Latest Threads

Top