debugcModerate
Returning result and error status for number type conversions
Viewed 0 times
resulterrornumberconversionsstatustypereturningforand
Problem
Looking for review of design strategy of using a
In constructing a library of functions, I've tried various error handling approaches, none completely satisfactory and was about to embark on this 5th strategy. (1-4 below.) Identifying any pitfalls or referencing any existing like-designed specification is sought.
Below is a small portion of a much larger conversion set of functions. The style/correctness of the function bodies is primarily illustrative of how functions may construct the combined
Typical other methods to receive a function result and error status include which I have tried.
-
Global
```
errno = 0;
char *endptr;
long num = strtol(some_string, &endptr,
struct to return the functional result and error status. Your experience of how my design approach may succeeded or failed is especially welcomed.In constructing a library of functions, I've tried various error handling approaches, none completely satisfactory and was about to embark on this 5th strategy. (1-4 below.) Identifying any pitfalls or referencing any existing like-designed specification is sought.
- Return result as a
structbundled with the status.
Below is a small portion of a much larger conversion set of functions. The style/correctness of the function bodies is primarily illustrative of how functions may construct the combined
struct result. In main(), one example use the returned structures value and status. The next just uses the value, showing a functional usage.#include
#include
#include
#include
typedef struct {
errno_t error;
double retval;
} e_double_type;
typedef struct {
errno_t error;
unsigned retval;
} e_unsigned_type;
e_double_type string_to_double(const char *src) {
char *endptr;
errno = 0;
e_double_type y = { 1, strtod(src, &endptr) };
if (endptr == src) return y;
if (errno && fabs(y.retval) > 1.0) return y;
y.error = 0;
return y;
}
e_unsigned_type longlong_to_unsigned(long long x) {
e_unsigned_type y = { 1, 0 };
if (x UINT_MAX) {
y.retval = UINT_MAX;
return y;
}
y.retval = (unsigned) x;
return y;
}
int main(void) {
// Error status and value
e_double_type y = string_to_double("123");
printf("%d %le\n", y.error, y.retval);
// Just value - status not of concern
// e_unsigned_type variable not needed
printf("%u\n", longlong_to_unsigned(123456789012345).retval +
longlong_to_unsigned(12345).retval);
return 0;
}Typical other methods to receive a function result and error status include which I have tried.
-
Global
errno: ```
errno = 0;
char *endptr;
long num = strtol(some_string, &endptr,
Solution
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).
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.
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
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
"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
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
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 timesCode Snippets
e_two_doubles_type ret = get_two_doubles();
// Was it ret.retval1 or ret.retval2 that I nedded?int sqlite3_open(
const char *filename, /* Database filename (UTF-8) */
sqlite3 **ppDb /* OUT: SQLite db handle */
);#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");enum { ... };
int function();
enum lib_error { ... );
enum lib_error function();
typedef enum { ... } lib_error;
lib_error function();Context
StackExchange Code Review Q#96419, answer score: 13
Revisions (0)
No revisions yet.