14
\$\begingroup\$

Looking for review of design strategy of using a 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.

  1. Return result as a struct bundled 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 <errno.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>

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 < 0) return y;
  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.

  1. Global errno:

    errno = 0;
    char *endptr;
    long num = strtol(some_string, &endptr, 10);
    if (errno) Handle_RangeError();
    
  2. Return special value:

    int ch = fgetc(stdin);
    if (ch == EOF) Handle_Problem();
    
  3. Pass address of error status and return result:

    int status;
    y = foo(x, &status);
    if (status) Handle_fooFailure();
    
  4. Pass address to store result and return error status:

    FILE *stream;
    errno_t status = fopen_s(&stream, filename, "r");
    if (status) Handle_OpenFailure();
    

The above methods, except #2, fail to keep the result and status together. #2 requires a wider return type that is often not available. The program model of using a function, where the parameters are only the inputs, and the result is the output is very appealing.

\$\endgroup\$
13
\$\begingroup\$

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.

\$\endgroup\$
  • \$\begingroup\$ Clarify: "might have meant to use the actual errno global.", No, the error was returned as part of the function's return value. Agree about short-comings of global errno. \$\endgroup\$ – chux - Reinstate Monica Jul 10 '15 at 0:34
  • \$\begingroup\$ @chux Do you mean if you should reuse #defines from the standard library? Like EINVAL, ENOENTetc \$\endgroup\$ – jacwah Jul 10 '15 at 0:39
  • \$\begingroup\$ Yes, my initial plan was to use error_t values like EINVAL, ENOENT. Alternatively some enumerated type could be used. In your experience, what approach worked well/failed in error schemes. \$\endgroup\$ – chux - Reinstate Monica Jul 10 '15 at 0:59
  • \$\begingroup\$ Concerning "a function has a multiple value result". In C, a function can only return one result, be it a double, pointer or struct, etc. This approach does oblige a new struct for each return type. But does not, prevent meaningful names to the two members without creating a new struct for every function (even with the same types). Example: consider div() which returns 2 double as div_t. This approach would have typedef struct { errno_t error; div_t retval; } e_div_type; So multiple functions re-use the same e_div_type. \$\endgroup\$ – chux - Reinstate Monica Jul 10 '15 at 13:44
  • \$\begingroup\$ @chux div() doesn't return two doubles in div_t: they're ints, but I'll pretend they are for the sake of argument. Imagine a function e_div_type dvec2_invert(double x, double y);. It takes as two coordinates of a vector as parameters, and return the inverse of it. They're still two doubles, but div_t is not a suitable type since the members are called quot and rem. Which is x and which is y? This is what I'm getting at in the text above. \$\endgroup\$ – jacwah Jul 10 '15 at 14:09
7
\$\begingroup\$

One typical pitfall is manifested in longlong_to_unsigned: it is too easy to miss a necessary update of the .error. Another one is a proliferation of otherwise useless structures.

I don't think there is a one-size-fits-all silver-bullet strategy. Each approach has its use and place.

\$\endgroup\$
  • \$\begingroup\$ Agree about the lack of a one-size-fits-all silver-bullet strategy - else this problem would have been solved long ago. \$\endgroup\$ – chux - Reinstate Monica Jul 10 '15 at 14:05
  • \$\begingroup\$ About the "missing" update of the .error: Skipping the .error field is intentional to show flexibility. If the calling code was amenable to longlong_to_unsigned() error handling (in this case: capping of overly-large values), than there is 1) no need to create an error value or pass NULL as in style #3. 2) no need to create a e_unsigned_type variable and 3) the .result field could be used immediately in an equation or chained function unlike styles #2 & #4. \$\endgroup\$ – chux - Reinstate Monica Jul 10 '15 at 14:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.