0

Just say I have list APIs, and provide find() to search node.

With this design if something went wrong during pre-conditions, client would have no idea about it because no status code provided.

bool find_node(list_t* me, int key)
{
    /*check pre-conditions*/
    ...
    ...

    /*if find return 1, otherwise return 0*/
}

I could just design this way

int find_node(list_t* me, int key, bool* result)
{
    /*check pre-conditions*/
    //fails return -1, otherwise return 0

    /*if find *result = 1, otherwise *result = 0*/
}

but I prefer client write code in this style

if (find_node(list, node_key))
{
    //do something
}

rather than

bool find_node = 0;
if (find_node(list, node_key, &find_node) == -1)
{
    //error handling
}

if (find_node)
{
    //do something
}

Is there any tip to solve this?

2 Answers 2

2

It really depends on what those preconditions represent.

Sometimes, violations of these preconditions are unrecoverable errors and the only appropriate response is to terminate the program. In essence, these preconditions would be checked via an assert(). For example, it makes no sense to return an error about using an uninitialized data structure – that is a programming error that must be fixed during development, it is not something that can be handled at runtime.

If violations are recoverable errors, then the error must be communicated to the developer in a difficult to misuse way. For example, the classic global errno strategy of communicating errors is extremely difficult to use correctly.

  • Silently swallowing the error is not a good strategy because you're returning an invalid result. This is extremely difficult to be usable correctly, and is likely to make downstream software buggy.

    An exception might be if those preconditions check for a specific input state, but other input states might have a reasonable default response. For example find_node(NULL, x) might reasonably return false since the NULL list does not contain any keys.

  • Returning a multi-valued result int find_node(...) with return values -1 (error), 0 (not found), 1 (found) is idiomatic C, but also easy to misuse: Since the -1 error result is a true value, people might forget to check this correctly.

  • Out-arguments make the error status more visible and thus more difficult to forget.

    • The function could return success/failure, possibly annotated with a compiler-specific note like GCC's __attribute__((warn_unused_result)).

    • Or it could be a void function that only communciates via out-arguments. While this makes the error status very visible, such a function also is cumbersome to use – it feels a bit like writing Go. For example:

      error err;
      bool found;
      find_nodes(list, key, &found, &err);
      if (err) { ... }
      if (found) { ... }
      

This is C, so it is unlikely that robust code can be written in an “elegant” manner. Later languages have included facilities for easier error handling. For example, C++ exceptions are perfect for communicating errors out-of-band without making it possible to accidentally ignore them, though it's not visible to a caller whether a function can throw. Rust instead makes it convenient to return fallible result types, where errors must be handled before the caller gets access to the actual result. But unless you like adding tons of macros into your code, these approaches can't be back-ported to C.

1

Effectively the only way C can manage an error channel is with return codes so you just have to lump it I'm afraid. Unless you're in a really specialised domain these days, I'd say the right option is not to be writing C.

[ Pedants: yes, I know C has setjmp and longjmp. Don't even think about using those though. ]

1
  • 1
    Don’t even think about mentioning them!
    – gnasher729
    Apr 9, 2022 at 16:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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