views:

248

answers:

9

My program is written in C for Linux, and has many functions with different patterns for return values:

1) one or two return n on success and -1 on failure.
2) some return 0 on success and -1 on failure.
3) some return 1 on success and 0 on failure (I generally spurn using a boolean type).
4) pointers return 0 on failure (I generally spurn using NULL).

My confusion arises over the first three -- functions that return pointers always return 0 on failure, that's easy.

The first option usually involves functions which return a length which may only be positive.

The second option, is usually involved with command line processing functions, but I'm unsure it has correctness, perhaps better values would be EXIT_SUCCESS and EXIT_FAILURE?

The third option is intended for functions which are convenient and natural to be called within conditions, and I usually emulate a boolean type here, by using int values 1 and 0.

Despite this all seeming reasonably sensible, I still find areas where this is not so clear or obvious as to which style to use when I create the function, or which style is in use when I wish to use it.

So how can I add clarity to my approach when deciding upon return types here?

+2  A: 

One condition I can think of where your above methodology can fail is a function that can return any value including -1, say a function to add two signed numbers.

In that case testing for -1 will surely be a bad idea.

In case something fails, I would better set a global error condition flag provided by the C standard in form of errno and use that to handle error.
Although, C++ standard library provides exceptions which takes off much hardwork for error handling.

Neeraj
A: 

I don't know if it helps but I'll give you the rules I tend to follow:

If the function should return a value e.g. int add(int x, int y) I return that value on success. If there is a sensible meaning for say 0 or -1 as a failure I would use that; otherwise (as in the above example) I would actually use int* add(int x, int y) and return a null pointer.

(Although that's a trivial example, you see what I mean...)

Where the function is more complicated or more likely to error (consider int openfile(char* filename, char* data)) then I tend to use pointers as arguments as done there and actually "return" through these. I use the int variable in an error code system, 0 being success and 1/2/3... being different errors, then error tracking can be more specific.

Likewise, with the whole application, I tend to use zero (EXIT_SUCCESS) as the "everything went ok" exit code and #define EXIT_FAIL_BECAUSE_NO_FILE 17 or something like that for the specific error should I not handle it internally (if it is fatal to the program like a failed malloc). Then, I document those codes.

That sounds to me like the top two above, mostly. With regards to the bottom two, for me, zero is always success, never one unless I'm using the bool data type, which I also don't do. With pointers, however, I always use null for an errornous result, however, unless I've got it wrong, zero is a null pointer? I use null anyway to avoid confusion - I know I'm dealing in pointers when I use null, zero isn't so clear.

Ninefingers
Who allocates the int? Who frees it?
jmucchiello
Rasmus Kaj
@jmucchiello - you don't allocate/free this pointer, it isn't an array. Init as NULL. Do not assign where int result = 0; before that.
Ninefingers
http://www.ivtools.org/ivtools/malloc.c - Malloc does this - see the struct for the bins - all the pointers are initially zero (null).
Ninefingers
A: 

So how can I add clarity to my approach when deciding upon return types here?

Just the fact that you're thinking about this goes a long way. If you come up with one or two rules - or even more if they make sense (you might need more than one rule - like you mention, you might want to handle returned pointers differently than other things) I think you'll be better off than many shops.

I personally like to have 0 returned to signal failure and non-zero to indicate success, but I don't have a strong need to hold to this. I can understand the philosophy that might want to reverse that sense so that you can return different reasons for the failure.

The most important thing is to have guidelines that get followed. Even nicer is to have guidelines that have a documented rationale (I believe that with rationales people are more likely to follow the guidelines). Like I said, just the fact that you're thinking about these things puts you ahead of many others.

Michael Burr
+2  A: 

Not an actual answer to your question, but some random comments you might find interesting:

  • it's normally obvious when to use case (1), but it gets ugly when unsigned types are involved - return (size_t)-1 still works, but it ain't pretty

  • if you're using C99, there's nothing wrong with using _Bool; imo, it's a lot cleaner than just using an int

  • I use return NULL instead of return 0 in pointer contexts (peronal preference), but I rarely check for it as I find it more natural to just treat the pointer as a boolean; a common case would look like this:

    struct foo *foo = create_foo();
    if(!foo) /* handle error */;
    
  • I try to avoid case (2); using EXIT_SUCCESS and EXIT_FAILURE might be feasible, but imo this approach only makes sense if there are more than two possible outcomes and you'll have to use an enum anyway

  • for more complicated programs, it might make sense to implement your own error handling scheme; there are some fairly advanced implementations using setjmp()/longjmp() around, but I prefer something errno-like with different variables for different types of errors

Christoph
A: 

Why don't you use the method used by the C standard library? Oh, wait...

Tim Schaeffer
Maybe because I'm unfamiliar with it, care to enlighten me?
James Morris
Tim is probably just beeing sarcastic about the fact that the C standard library isn't really consistent on this point either ... :-)
Rasmus Kaj
Ah I see, must keep that paranoia in check...
James Morris
FTR: Rasmus is right, I was being sarcastic. But if you wish for enlightenment, study the sacred scrolls:http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1336.pdf
Tim Schaeffer
+2  A: 

For can't fail deterministic yes/no responses using more specific (bool) return type can help maintain consistancy. Going further for higher level interfaces one may want to think about returning or updating a systems specific messaging/result detail structure.

My preference for 0 to always be success is based on the following ideas:

  1. Zero enables some basic classing for organizing failures by negative vs positive values such as total failure vs conditioned success. I don't recommend this generally as it tends to be a bit too shallow to be useful and might lead to dangerous behaviorial assumptions.

  2. When success is zero one can make a bunch of orthagonal calls and check for group success in a single condition later simply by comparing the return code of the group..

    rc = 0; rc += func1(); rc += func2(); rc += func3(); if (rc == 0) success!

  3. Most importantly zero from my experience seems to be a consistant indication of success when working with standard libraries and third party systems.

Einstein
A: 

That is a matter of preference, but what I have noticed is the inconsistency. Consider this using a pre C99 compiler

#define SUCCESS  1
#define ERROR    0

then any function that returns an int, return either one or the other to minimize confusion and stick to it religiously. Again, depending on, and taking into account of the development team, stick to their standard.

In pre C99 compilers, an int of zero is false, and anything greater than zero is to be true. That is dependant on what standard is your compiler, if it's C99, use the stdbool's _Bool type.

The big advantage of C is you can use your personal style, but where team effort is required, stick to the team's standard that is laid out and follow it religiously, even after you leave that job, another programmer will be thankful of you.

And keep consistent.

Hope this helps, Best regards, Tom.

tommieb75
+2  A: 

So how can I add clarity to my approach when deciding upon return types here?

Pick one pattern per return type and stick with it, or you'll drive yourself crazy. Model your pattern on the conventions that have long been established for the platform:

  • If you are making lots of system calls, than any integer-returning function should return -1 on failure.

  • If you are not making system calls, you are free to follow the convention of the C control structures that nonzero means success and zero means failure. (I don't know why you dislike bool.)

  • If a function returns a pointer, failure should be indicated by returning NULL.

  • If a function returns a floating-point number, failure should be indicated by returning a NaN.

  • If a function returns a full range of signed and unsigned integers, you probably should not be coding success or failure in the return value.

Testing of return values is a bane to C programmers. If failure is rare and you can write a central handler, consider using an exception macro package that can indicate failures using longjmp.

Norman Ramsey
A: 

Much of the C standard library uses the strategy to only return true (or 1) on success and false (or 0) on failure, and store the result in a passed in location. More specific error codes than "it failed" is stored in the special variable errno.

Something like this int add(int* result, int a, int b) which stores a+b in *result and returns 1 (or returns 0 and sets errno to a suitable value if e.g. a+b happens to be larger than maxint).

Rasmus Kaj