I don't like the idea of using an all-in-one struct. You will have to do more maintenance: every time you create a function that returns different values, you have to create a new struct.
Further, I think the struct return value will confuse the user. Everyone is familiar with the basic types, so they will immediately know how to use a function just from it's signature. When you introduce non-opaque structs, the user also has to look up the documentation for that, on top of the documentation for the actual function.
Think about when a function has a multiple value result - you can't give meaningful names to the two members without creating a new struct for every function (even with the same types).
e_two_doubles_type ret = get_two_doubles();
// Was it ret.retval1 or ret.retval2 that I nedded?
My preferred way to signal status is to return a constant #defined in a header. For an example look at the API of SQLite. It's a library I've used in projects, and it has worked really well. I also use this pattern for my own code. The results of the functions are returned through pointer parameters. This strategy is simple and will feel intuitive to users since it's so common.
int sqlite3_open(
const char *filename, /* Database filename (UTF-8) */
sqlite3 **ppDb /* OUT: SQLite db handle */
);
The standard C library uses a "global variable" for error status. It is nowadays actually a macro, because globals simply aren't thread safe. If you want your API to be able to be used by multiple threads at the same time, global state is a no-no. There is no way to know which call generated the error code if you can't know the exact order of operations. I fear implementing your own errno as a thread-local macro will be more trouble than it's worth.
Originally this was a static memory location, but macros are almost always used today to allow for multi-threading, such that each thread will see its own error number. (Wikipedia)
When re-reading your question I think you might have meant to use the actual errno
global. This is feasible, but might surprise users. Normally, errno
is only ever used by the standard library. I've seen one system implement their own version, but I don't see any advantages over returning the value directly.
The "return special value" approach can work in some cases. You need a value that is not a valid result, and must thus either artificially limit the domain of the function, or use a wider type than otherwise necessary. The obvious case is the NULL
pointer, which is handy if there is only a single type of error, or if you don't need to know why the call failed.
"Pass address of error status" is similar to my proposed solution, it's just the other way around. The problem with this is that you can only return one value. There will only ever be one status, in contrast to results, so why not return the status instead?
You mention keeping status and result together. I don't agree with this goal, they are very different things and should be kept apart. The status is needed right after the call, but the result could live on for the rest of the program. Tightly coupling these will make continued use of the result awkward.
Consistency is key to making a great API. Make the user feel at home when using it, let their presumptions about your function's signatures be right. To achieve this, you need a consistent way to return status and results of functions. That's where "return status, reference results" shines, because it can be adapted to all situations - no matter how many results or of what types, the same basic principle is there.
When it comes to the status values themselves, you've pointed out two, and I've mentioned one approach above.
I don't like the idea of reusing standard errno values. Those values are specialised for system calls and most values probably won't be usable by your API. Here is a chart of values defined for several platforms. Note that different constants are available for different platforms. Imagine for instance that you want to use EMSGSIZE
(Message too long). This would work fine for all platforms except for Microsoft's compiler (MSVC). Now you need to check for #ifdef _WIN32
and define it yourself. This is even more work than just coming up with your own suite of constants.
Extending the standard values is also bad. What if a platform you didn't think of has used this identifier already? Your library and this platform will be incompatible until you change it. Always try to avoid clashing with platforms, even if you don't think you want to support it today.
Names beginning with a capital ‘E’ followed a digit or uppercase letter may be used for additional error code names. (GNU libc manual)
Further, errno values are not very readable. Their names are dense and hard to remember. I think EACCES
is a great example, or rather a horrific one. They've intentionally misspelled "access" - that has thrown me off a couple of times.
Now we've ruled out that one, there are two left. You've decided to create your own constants, but should they be #defined enum-erated? I think both are good choices, but I'll go over the pros and cons of them below.
With macros, you have to specify each value by hand. This is both a strength and a weakness. It's dangerous because you can mistakenly #define two macros to the same value, without any compile time errors or warnings (I tried this with gcc -Wall). In effect, this bug can be hard to find.
#define LIB_OUT_OF_RANGE 86723
#define LIB_INVALID_STRING 86723
int status = string_to_double(str, &dbl);
if (status == LIB_OUT_OF_RANGE)
fprintf(stderr, "Too large, try again\n");
else if (status == LIB_INVALID_STRING)
fprintf(stderr, "Please input a number\n");
The second if-statement will never ever be executed, because LIB_OUT_OF_RANGE == LIB_INVALID_STRING
. The compiler will probably even optimize the second if statement away.
The advantage is that you will always know what value a certain constant has, as opposed to enum constants. Try counting all prior constants to get the value in an enum with 50+ values.
Knowing the values is useful for debugging and logging: you can just print %d the values and check them against your header.
If you decide to go with enums, there are three different ways to do it. They are list below in order of transparency.
enum { ... };
int function();
enum lib_error { ... );
enum lib_error function();
typedef enum { ... } lib_error;
lib_error function();
I like to be upfront with what the user is getting. The enum/typedef is explicit with how values of them are to be interpreted, but not with the underlying type. I like to know what's an int and what's something else, but you might differ. I think any of these are fine.
In summary, use either macros or enums. Both are fine and each have their pros and cons.
PS. If you want to read up on a nifty way to pair integer constants with their string representation, check out this article on X Macros.