2

I'm working on a little pet program in C where I have a game board that consists of a bunch of squares:

typedef struct _square {
    bool checked;
} Square;

typedef struct _board {
    Square *squares;
    uint8_t width;
    uint8_t height;
} Board;

I don't want these structs to be visible to the public, so instead I want to write a simple function that checks if a square is checked or not, called square_is_checked. It makes sense for the return type to be a bool (I'm assuming C99 here).

bool square_is_checked(Board *b, uint8_t row, uint8_t col)
{
    return b->squares[(uint16_t)row * b->height + col].checked;
}

However, it's possible that a NULL pointer, or a row and column that don't exist could be passed as arguments. So I want to add some simple checks:

bool square_is_checked(Board *b, uint8_t row, uint8_t col)
{
    if (!b) { return ???; }
    if (row >= b->height || col >= b->width) { return ???; }
    return b->squares[(uint16_t)row * b->height + col].checked;
}

However, a bool is either true or false -- no room for error management. What's considered best practice here (e.g., do I have to rewrite this function to pass a pointer to a bool)?

5
  • The right thing to do will depend on who will be calling square_is_checked, and is it reasonable to expect them to ensure their inputs are valid or not. Commented Sep 11, 2017 at 2:31
  • 3
    Well, the conventional C thing to do is "you crash". Commented Sep 11, 2017 at 4:01
  • 1
    C has a bool type? What they won't think of!! In the old days, you could simply return -1 from your Boolean function to indicate errors ;)
    – Erik Eidt
    Commented Sep 11, 2017 at 5:55
  • 1
    That's a good example why other languages have exceptions... Commented Sep 11, 2017 at 10:10
  • @user253751 If it is a bug to call the function with the NULL pointer argument, then "you crash" is actually very good handling. Forces the developer to fix the bug.
    – gnasher729
    Commented Apr 1, 2022 at 9:36

4 Answers 4

9

This isn't really about how to return bool from a function in C. Your code already does that.

The question really is, is bool the right data type when the set of possible values is larger than true and false. That could be the subject of a lengthy philosophical discussion, but as this is a personal project, you don't need to worry about your decision within a larger context. On a team, where existing code might already have an opinion on situations like this, a decision might be made for you.

So in this case, there are many couple possible answers. A few options:

  • Keep the bool return type, and use false for input errors such as null pointers or invalid values (row > height, etc.)
  • Use an enum to represent CHECKED, UNCHECKED, UNKNOWN etc. instead of a boolean. Many C libraries effectively do this by returning 0 (e.g. INVALID_SOCKET) to indicate an error (or sometimes -1).
  • Validate the inputs elsewhere to ensure that this function can always correctly return a meaningful boolean when called.
6

Some other options include passing in a pointer to a bool and returning an error value. Something like this:

typedef enum square_err_t {
    ERROR_NONE = 0,
    ERROR_BAD_PTR,
    ERROR_BAD_ROW,
    ERROR_BAD_COL
} square_err_t;

square_err_t square_is_checked(Board* b, uint8_t row, uint8_t col, bool* is_checked)
{
    ...
}

Since this is a personal project, you might be able to get away with doing your checks as preconditions via assert:

bool square_is_checked(Board* b, uint8_t row, uint8_t col)
{
    assert(b != NULL);
    assert(col < b->width);
    assert(row < b->height);
    ...etc.
}

This will catch errors during development, but the checks will be compiled out in release.

If your compiler or other tooling supports it, you might be able to annotate your functions with nullability annotations. (That example is for Swift, but a quick search shows tools that support them available for other languages, too.) These allow you to mark arguments to a function as nonnull, meaning the argument is not allowed to be NULL. This will catch some potential calls at compile time rather than runtime and let you fix them before you ship.

2
  • 1
    I think returning an error and accepting a bool pointer might be acceptable for a library call. If OP's using it internally, the proper thing to do is to do no checks whatsoever. If anything, you ensure that should it be called incorrectly, that it does end up in an error and doesn't say, return a boolean to indicate that everything is fine and dandy when it isn't. Fail fast is a perfectly reasonable solution that doesn't slow anything down (I think you cover this with your assert precondition checks).
    – Neil
    Commented Sep 11, 2017 at 7:31
  • null pointers often have an assert built in. Or at least "check if the pointer is null and crash if it is". So. compiling out an assert checking for a null pointer in a release build is often pointless.
    – gnasher729
    Commented Apr 1, 2022 at 9:39
2

There is a fundamental decision for you to make:

Is an invalid input a violation of the contract, or expected?

You already depend on the caller to give a pointer to a Board, and the right one at that. Why do you want to special-case the invalid input NULL, aren't that all logic-errors by the programmer?

An out-of-range board-position can be argued about much more:

  1. Is it a logic-error committed by the programmer?
  2. Is it user-input failing validation?
  3. Is there a logical continuation of the given board?

There are three ways to react to logic-errors:

  1. Panic. assert() in debug-builds (be generous with assert, as it is a no-op in release), abort() in release. Depending on the severity of the error, in moderately safe languages with exceptions throwing an unchecked exception might qualify.
  2. Let the dice fall where they will. That's what Undefined Behavior means, and allows by far for the highest efficiency.
  3. Try to substitute sensible defaults and muddle through or return an error. In languages with exceptions, exceptions might be the way you return an error.
-3

Maybe take a look at   man errno   and use one of the two strategies discussed there. Either   int square_is_checked(etc)   and   return -1   for errors, or else set   errno   to signal errors, as discussed by the man page. (Personally, I usually go with the -1 in these kinds of situations. You can use int just as well as your bool.)

1
  • 3
    errno is an anti-pattern. Its use should not be encouraged at all. Commented Sep 11, 2017 at 3:44

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.