Error handling in C code

2019-01-01 16:41发布

What do you consider "best practice" when it comes to error handling errors in a consistent way in a C library.

There are two ways I've been thinking of:

Always return error code. A typical function would look like this:

MYAPI_ERROR getObjectSize(MYAPIHandle h, int* returnedSize);

The always provide an error pointer approach:

int getObjectSize(MYAPIHandle h, MYAPI_ERROR* returnedError);

When using the first approach it's possible to write code like this where the error handling check is directly placed on the function call:

int size;
if(getObjectSize(h, &size) != MYAPI_SUCCESS) {
  // Error handling
}

Which looks better than the error handling code here.

MYAPIError error;
int size;
size = getObjectSize(h, &error);
if(error != MYAPI_SUCCESS) {
    // Error handling
}

However, I think using the return value for returning data makes the code more readable, It's obvious that something was written to the size variable in the second example.

Do you have any ideas on why I should prefer any of those approaches or perhaps mix them or use something else? I'm not a fan of global error states since it tends to make multi threaded use of the library way more painful.

EDIT: C++ specific ideas on this would also be interesting to hear about as long as they are not involving exceptions since it's not an option for me at the moment...

21条回答
临风纵饮
2楼-- · 2019-01-01 17:23

I definitely prefer the first solution :

int size;
if(getObjectSize(h, &size) != MYAPI_SUCCESS) {
  // Error handling
}

i would slightly modify it, to:

int size;
MYAPIError rc;

rc = getObjectSize(h, &size)
if ( rc != MYAPI_SUCCESS) {
  // Error handling
}

In additional i will never mix legitimate return value with error even if currently the scope of function allowing you to do so, you never know which way function implementation will go in the future.

And if we already talking about error handling i would suggest goto Error; as error handling code, unless some undo function can be called to handle error handling correctly.

查看更多
只若初见
3楼-- · 2019-01-01 17:25

I've used both approaches, and they both worked fine for me. Whichever one I use, I always try to apply this principle:

If the only possible errors are programmer errors, don't return an error code, use asserts inside the function.

An assertion that validates the inputs clearly communicates what the function expects, while too much error checking can obscure the program logic. Deciding what to do for all the various error cases can really complicate the design. Why figure out how functionX should handle a null pointer if you can instead insist that the programmer never pass one?

查看更多
梦该遗忘
4楼-- · 2019-01-01 17:25

There's a nice set of slides from CMU's CERT with recommendations for when to use each of the common C (and C++) error handling techniques. One of the best slides is this decision tree:

Error Handling Decision Tree

I would personally change two things about this flowcart.

First, I would clarify that sometimes objects should use return values to indicate errors. If a function only extracts data from an object but doesn't mutate the object, then the integrity of the object itself is not at risk and indicating errors using a return value is more appropriate.

Second, it's not always appropriate to use exceptions in C++. Exceptions are good because they can reduce the amount of source code devoted to error handling, they mostly don't affect function signatures, and they're very flexible in what data they can pass up the callstack. On the other hand, exceptions might not be the right choice for a few reasons:

  1. C++ exceptions have very particular semantics. If you don't want those semantics, then C++ exceptions are a bad choice. An exception must be dealt with immediately after being thrown and the design favors the case where an error will need to unwind the callstack a few levels.

  2. C++ functions that throw exceptions can't later be wrapped to not throw exceptions, at least not without paying the full cost of exceptions anyway. Functions that return error codes can be wrapped to throw C++ exceptions, making them more flexible. C++'s new gets this right by providing a non-throwing variant.

  3. C++ exceptions are relatively expensive but this downside is mostly overblown for programs making sensible use of exceptions. A program simply shouldn't throw exceptions on a codepath where performance is a concern. It doesn't really matter how fast your program can report an error and exit.

  4. Sometimes C++ exceptions are not available. Either they're literally not available in one's C++ implementation, or one's code guidelines ban them.


Since the original question was about a multithreaded context, I think the local error indicator technique (what's described in SirDarius's answer) was underappreciated in the original answers. It's threadsafe, doesn't force the error to be immediately dealt with by the caller, and can bundle arbitrary data describing the error. The downside is that it must be held by an object (or I suppose somehow associated externally) and is arguably easier to ignore than a return code.

查看更多
只靠听说
5楼-- · 2019-01-01 17:25

Here is an approach which I think is interesting, while requiring some discipline.

This assumes a handle-type variable is the instance on which operate all API functions.

The idea is that the struct behind the handle stores the previous error as a struct with necessary data (code, message...), and the user is provided a function that returns a pointer tp this error object. Each operation will update the pointed object so the user can check its status without even calling functions. As opposed to the errno pattern, the error code is not global, which make the approach thread-safe, as long as each handle is properly used.

Example:

MyHandle * h = MyApiCreateHandle();

/* first call checks for pointer nullity, since we cannot retrieve error code
   on a NULL pointer */
if (h == NULL)
     return 0; 

/* from here h is a valid handle */

/* get a pointer to the error struct that will be updated with each call */
MyApiError * err = MyApiGetError(h);


MyApiFileDescriptor * fd = MyApiOpenFile("/path/to/file.ext");

/* we want to know what can go wrong */
if (err->code != MyApi_ERROR_OK) {
    fprintf(stderr, "(%d) %s\n", err->code, err->message);
    MyApiDestroy(h);
    return 0;
}

MyApiRecord record;

/* here the API could refuse to execute the operation if the previous one
   yielded an error, and eventually close the file descriptor itself if
   the error is not recoverable */
MyApiReadFileRecord(h, &record, sizeof(record));

/* we want to know what can go wrong, here using a macro checking for failure */
if (MyApi_FAILED(err)) {
    fprintf(stderr, "(%d) %s\n", err->code, err->message);
    MyApiDestroy(h);
    return 0;
}
查看更多
刘海飞了
6楼-- · 2019-01-01 17:26

When I write programs, during initialization, I usually spin off a thread for error handling, and initialize a special structure for errors, including a lock. Then, when I detect an error, through return values, I enter in the info from the exception into the structure and send a SIGIO to the exception handling thread, then see if I can't continue execution. If I can't, I send a SIGURG to the exception thread, which stops the program gracefully.

查看更多
裙下三千臣
7楼-- · 2019-01-01 17:27

I like the error as return-value way. If you're designing the api and you want to make use of your library as painless as possible think about these additions:

  • store all possible error-states in one typedef'ed enum and use it in your lib. Don't just return ints or even worse, mix ints or different enumerations with return-codes.

  • provide a function that converts errors into something human readable. Can be simple. Just error-enum in, const char* out.

  • I know this idea makes multithreaded use a bit difficult, but it would be nice if application programmer can set an global error-callback. That way they will be able to put a breakpoint into the callback during bug-hunt sessions.

Hope it helps.

查看更多
登录 后发表回答