DEV Community

Paul J. Lucas
Paul J. Lucas

Posted on

Avoid the Temptation of bool

Introduction

The Boolean type (spelled bool in C, C++, Go, and Rust, among others; and boolean in Java) is fundamental to programming since so many things are either true or false. Of course you can use bool for function parameters, and it’s tempting to do so — but you probably shouldn’t. Consider a function call like:

void *buf = alloc_buf( 4096, true );  // What does "true" mean?
Enter fullscreen mode Exit fullscreen mode

Seeing a literal true or false as part of a function call tells you nothing about what it means. Does it mean the buffer is maximally aligned? Zeroed? Prints an error message if the allocation fails? You have no idea. You have to find and read the either function’s declaration or documentation to know what it means:

void* alloc_buf( size_t size, bool zero_buf );
Enter fullscreen mode Exit fullscreen mode

It’s for this reason I personally try to avoid using bool for parameters in public APIs. Code is read far more than it’s written, so clarity matters. But what can you do instead of using bool? That’s what this article is about.

This article’s examples are specifically in C but apply equally well to C-like languages or any language that has a Boolean type.

Alternatives to bool

There are a few alternatives depending on circumstances and taste.

Use Two Functions

Instead of a single function, have two:

void* alloc_raw( size_t size );
void* alloc_zero( size_t size );
Enter fullscreen mode Exit fullscreen mode

Note that this would be in the public API. Internally, you can still implement a single function that takes a bool if you want and have the public functions just call the internal function.

Use an Enumeration

Instead of using bool, define an enumeration:

enum alloc_opts {
  ALLOC_RAW,
  ALLOC_ZEROED
};
typedef enum alloc_opts alloc_opts;

void* alloc_buf( size_t size, alloc_opts opt );
Enter fullscreen mode Exit fullscreen mode

Of all the languages mentioned, only Go doesn’t directly support enumerations — but you can fake it.

You might think creating an enumeration to replace a bool is overkill, but now look how the function is called:

void *buf = alloc_buf( 4096, ALLOC_ZERO );
Enter fullscreen mode Exit fullscreen mode

It’s barely longer, but much clearer. Investing more in functions’ APIs has a many-fold return on said investment in terms of clarity.

Another benefit of using an enumeration is that it allows for additional options to be added easily by converting the enumeration to use bit flags:

enum alloc_opts {
  ALLOC_RAW     =      0,
  ALLOC_ALIGNED = 1 << 0,
  ALLOC_ZEROED  = 1 << 1,
};
Enter fullscreen mode Exit fullscreen mode

Then you can do things like:

void *buf = alloc_buf( 4096, ALLOC_ALIGNED | ALLOC_ZERO );
Enter fullscreen mode Exit fullscreen mode

Note that code already using the function doesn’t have to change (unless you want to use the new options); it just has to be recompiled.

Use Comments (as a Last Resort)

If your code either calls a 3rd-party function that takes a bool parameter or you simply can’t change your function’s API for whatever reason, at least use inline comments that repeat the name of the parameters:

void *buf = alloc_buf( 4096, /*zero_buf=*/true );
Enter fullscreen mode Exit fullscreen mode

Programmers reading your code (including yourself in several months’ time) will thank you.

Conclusion

Since bool is so easy, it’s very tempting to use it for function parameters, especially when retrofitting an existing function just to tweak it. Avoid the temptation: either add a second function or use an enumeration.

Top comments (3)

Collapse
 
kurealnum profile image
Oscar

I always go for enums in situations like this. Especially with Rust's inline docs, being able to instantly see what the enum is for and what a certain variant does makes my life a lot easier.

Collapse
 
lolpopgames profile image
LolPopGames

I always try to avoid enums and use uint8_t & macros instead, as enums take up 2 or 4 bytes of memory (depending on the compiler and architecture), and often 1 byte is enough to store such data. But I'll likely switch to them everywhere except preprocessor programming.

Collapse
 
pauljlucas profile image
Paul J. Lucas • Edited

Unless you have special circumstances, using an extra few bytes for a parameter (that's passed on the stack or, more likely, in a register, i.e., temporary), it simply doesn't matter. But if you insist, starting in C23, you can specify the underlying type of an enum; same with C++:

enum alloc_opts : uint8_t {
  // ...
Enter fullscreen mode Exit fullscreen mode

The other advantage of using an enum is that gdb and lldb are smart enough to print their names when debugging rather than their underlying integer values — even when using bit fields.