Ways to rewrite boolean parameters in C++

Programmers read code a lot more than they write it, so it’s important to write clean, consistent, unambiguous code. The author of C++17 in detail wrote about ways to avoid confusion. We share his material for the start of the course on development in C++.


Boolean parameters in functions can be misleading and make code difficult to read if the function name is not descriptive:

DoImportantStuff(true, false, true, false);

It is not clear what these parameters mean. What does the first true or the last false mean? Is it possible to improve the code in such cases? Let’s look at a possible refactoring.

Introduction

This article is inspired by a similar post that appeared on Andrzej Krzemenski’s blog: Toggles in functions. As writes Andrzejthe whole point is to improve the code of such functions:

RenderGlyphs(glyphs, true, false, true, false);

What if we change the order of the parameters? Then the compiler won’t help much. Let’s think about how to make the code better: make it safer and more readable. Let’s add comments:

RenderGlyphs(glyphs,
             /*useChache*/true, 
             /*deferred*/false, 
             /*optimize*/true, 
             /*finalRender*/false);

And although the code above reads a little better, there is still room for making it safer. But can more be done?

Ideas

Here are some ideas:

Tiny transfers

Let’s write an ad like this:

enum class UseCacheFlag    { False, True };
enum class DeferredFlag    { False, True };
enum class OptimizeFlag    { False, True };
enum class FinalRenderFlag { False, True };

// и вызов, например:
RenderGlyphs(glyphs,
             UseCacheFlag::True, 
             DeferredFlag::False, 
             OptimizeFlag::True, 
             FinalRenderFlag::False);

And you need to change the implementation:

if (useCache) { }
else { }
if (deferred) { }
else {}

Compare code:

if (useCache == UseCacheFlag::True) { }
else { }
if (deferred == DeferredFlag::True) { }
else {}

As you can see, we now need to check enum values, not just bool values. Using enums is a good approach, but it has its drawbacks:

  • Values ​​are not directly converted to booleans, so Flag::True must be compared explicitly, inside the function body.

The required explicit comparison is the reason for the small libraries Andrzej, who creates bool convertible switches. I was disappointed by the language’s lack of direct support for strong typing for enums. But later I began to think differently.

Explicit comparison isn’t hard to write, so perhaps including strong typing in the language is overkill? Explicit type conversion can even cause some problems. However, I’m not entirely happy with having to write so many tiny enums…

bit flags

Bit flags can be used as a potential development of enums. Unfortunately, we don’t have friendly and type-safe support for them in the language, so we need to add some boilerplate.

Here is a simplified approach:

#include <type_traits>

struct Glyphs { };

enum class RenderGlyphsFlags
{
    useCache = 1,
    deferred = 2, 
    optimize = 4,
    finalRender = 8,
};

// упрощение...
RenderGlyphsFlags operator | (RenderGlyphsFlags a, RenderGlyphsFlags b) {
    using T = std::underlying_type_t <RenderGlyphsFlags>;
    return static_cast<RenderGlyphsFlags>(static_cast<T>(a) | static_cast<T>(b));
    // todo: пропущенные проверки, находится ли значение в нужном диапазоне...
}

constexpr bool IsSet(RenderGlyphsFlags val, RenderGlyphsFlags check) {
    using T = std::underlying_type_t <RenderGlyphsFlags>;
    return static_cast<T>(val) & static_cast<T>(check);
    // todo: пропущенные дополнительные проверки...
}

void RenderGlyphs(Glyphs &glyphs, RenderGlyphsFlags flags)
{
    if (IsSet(flags, RenderGlyphsFlags::useCache)) { }
    else { }

    if (IsSet(flags, RenderGlyphsFlags::deferred)) { }
    else { }

    // ...
}

int main() {
    Glyphs glyphs;
    RenderGlyphs(glyphs, RenderGlyphsFlags::useCache | RenderGlyphsFlags::optimize);                                      
}

You can experiment with the code in @CompilerExplorer.

What do you think of this approach? With some extra code and operator overloading, you can end up with a type-safe, readable, and beautiful function. By adding a check to my code, you make sure that the right bit is set in the values ​​being passed.

Since C++23, you can use std::to_underlying() from the header file. This feature is already implemented in GCC, Clang and MSVC: see my example in @CompilerExplorer.

Structure Options

If you have multiple parameters, four or five depending on the context, why not wrap them in separate structures?

struct RenderGlyphsParam
{
    bool useCache;
    bool deferred;
    bool optimize;
    bool finalRender;
};
void RenderGlyphs(Glyphs &glyphs, const RenderGlyphsParam &renderParam);

// вызов:
RenderGlyphs(glyphs,
             {/*useCache*/true, 
             /*deferred*/false, 
             /*optimize*/true, 
             /*finalRender*/false});

It didn’t help much. This resulted in additional control code, and the caller uses almost the same code. Yes, this approach has the following advantages:

  • He moves the problem. You can apply strong typing to individual members of a structure.

  • If you need more parameters, you can expand the structure.

  • The approach is especially useful when many functions contain the same structure elements.

The variable glyphs can be put into RenderGlyphsParam, this is just an example.

And what about C++20?

Thanks to the designated initializers introduced in C++20, you can use named parameters when constructing a structure. In general, when naming parameters passed to a function, you can use the same approach as in C99 argument names:

struct RenderGlyphsParam
{
    bool useCache;
    bool deferred;
    bool optimize;
    bool finalRender;
};
void RenderGlyphs(Glyphs &glyphs, const RenderGlyphsParam &renderParam);

// вызов:
RenderGlyphs(glyphs,
             {.useCache = true, 
              .deferred = false, 
              .optimize = true, 
              .finalRender = false}); 

View code in @CompilerExplorer.

You can read about this new functionality in my post Designated Initializers in C++20.

Removing Boolean Parameters

We can try to fix the syntax and write a clear method. But what if the method is even simpler? Provide more features and just eliminate the parameter?

It’s OK to have one or two switch parameters, but having more than that could mean the function is trying to do too much. In our simple example, let’s try the following split:

RenderGlyphsDeferred(glyphs,
             /*useCache*/true, 
             /*optimize*/true);
RenderGlyphsForFinalRender(glyphs,
             /*useCache*/true, 
             /*optimize*/true;

Let’s make a change in mutually exclusive parameters: deferred and final are not executed at the same time. If you can’t split the code, you can have an external RenderGlyphsInternal function.

This function would still accept these switch parameters. But at least such internal code will be hidden from the public API. If possible, rewrite the outer function later.

I think it’s useful to look at the declaration of a function and revisit it for mutually exclusive parameters. Maybe the function is doing too much? If so, break it down into smaller functions.

In writing this section, I have taken note of Martin Fowler’s advice in articlewhere it also tries to avoid switches. You can read this article here and more in book Clean Code: A Handbook of Agile Software Craftsmanship.

Reinforced types

Tiny enums are part of the larger topic of applying strong typing. Similar problems can appear when your parameters are multiple integers or strings. Read the details here:

C++ Guides

Luckily, we have C++ manuals that we can turn to for help. Here is one of them: I.4: Make interfaces precisely and strongly typed, it talks not only about boolean parameters, but about all the potentially misleading names. For example:

draw_rect(100, 200, 100, 500); // what do the numbers specify?

draw_rect(p.x, p.y, 10, 20); // what units are 10 and 20 in?

To make the code better, we will use the following approaches:

  • Let’s pass a separate structure so that the arguments are converted to data members.

  • Consider the use of enumeration flags.

  • Let’s pass std::chrono::milliseconds to some function, not int num_msec.

Moreover, below are the mandatory rules suggested by code analysis tools: look at a function with many primitive arguments.

Instruments

Speaking of tools, one reader offered Clang-Tidy check, which makes write “named type comments” next to the arguments. This functionality is called bugprone-argument-comment.

An example of her work:

void RenderGlyphs(Glyphs &glyphs, 
  bool useCache, bool deferred, bool optimize, bool finalRender, int bpp)
{
 
}

int main() {
    Glyphs glyphs;
    RenderGlyphs(glyphs,
             /*useCha=*/true, 
             /*deferred=*/false, 
             /*optimize=*/true, 
             /*finalRender=*/false,
             /*bpppp=*/8);
                                    
}

You will receive this message:

<source>:13:14: warning: argument name 'useCha' in comment does not 
          match parameter name 'useCache' [bugprone-argument-comment]
             /*useCha=*/true, 
             ^
<source>:5:8: note: 'useCache' declared here
  bool useCache, bool deferred, bool optimize, bool finalRender, int bpp)
       ^

The form of the comment should be like this: /*arg=*/. Look at the example in @CompilerExplorer.

Specific example

I recently had the opportunity to apply some enum/strong type ideas to my code. Here’s a rough sketch:

// функции:
bool CreateContainer(Container *pOutContainer, bool *pOutWasReused);

void Process(Container *pContainer, bool bWasReused);

// применение
bool bWasReused = false;
if (!CreateContainer(&myContainer, &bWasReused))
   return false;

Process(&myContainer, bWasReused);

Briefly: a container is created and processed. It can be reused through pooling, object reuse, internal logic, etc. I think it’s ugly. A flag is used, then it is passed to some other function.

Moreover, we are passing pointers, and there should be additional validation. Also, output parameters are daunting in modern C++, so it’s still not a good idea. Can it be done better? Yes. With enums:

enum class ContainerCreateInfo { Err, Created, Reused };
ContainerCreateInfo CreateContainer(Container *pOutContainer);

void Process(Container *pContainer, ContainerCreateInfo createInfo);

// применение
auto createInfo = CreateContainer(&myContainer)
if (createInfo == ContainerCreateInfo::Err);
   return false;

Process(&myContainer, createInfo);

There is no pointer derivation here. There is a strong type for the switch parameter. If more information needs to be passed to the CreateInfo enum, you can simply add an enum member and process it in the appropriate place; function prototypes should not change.

Of course, in the implementation, you can compare enum values, and not just bool, but this is not difficult and even more detailed. The code is still imperfect because there is a pOutContainer which is not perfect.

In my real project, this was hard to change and I wanted to reuse existing containers. But if your container supports move semantics and you can rely on return value optimization, then it’s possible to return it:

enum class ContainerCreateInfo { Err, Created, Reused };
std::pair<Container, ContainerCreateInfo> CreateContainer();

Our function becomes a function factory, even returning additional information about the creation process. You can use it like this:

// применение
auto [myContainer, createInfo] = CreateContainer()
if (createInfo == ContainerCreateInfo::Err);
   return false;

Process(&myContainer, createInfo);

Summary

Reading the original article Andrzej and these additions from me, I hope you get the idea of ​​switch parameters. They are not entirely wrong, and it is probably not possible to avoid them entirely.

It’s still good to revisit your design if you want to add three or four options in a row. Maybe you can reduce the number of switches/flags, resulting in more expressive code.

Reading List:

Few questions:

Share your feedback in the comments.

And we will help you improve your skills and master a profession that will remain in demand at any time:

Choose another in-demand profession.

Brief catalog of courses and professions

Data Science and Machine Learning

Python, web development

Mobile development

Java and C#

From basics to depth

As well as

Similar Posts

Leave a Reply Cancel reply