Passing an enum by parameter

Author: Chloé Lourseyre

Let’s say you have a function you have to call using two parameters :

  • The first parameter is a configuration option symbolized by an enum. Like the “mode of use” of your function.
  • The second parameter is a true numeral parameter.

Let’s use the following example to represent that :

#include <iostream>

// Enum describing the mode used in the following function
enum class ChangeMode
{
    Before,
    After
};

// Function that squares a value and increases it (wether before or after), then prints the result
void increase_and_square(const ChangeMode& m, int v)
{
    if (m == ChangeMode::Before)
        ++v;
    
    v = v*v;
    
    if (m == ChangeMode::After)
        ++v;

    std::cout << v << std::endl; 
}

// main function
int main()
{
    const int a = 42;
    increase_and_square(ChangeMode::Before, a);
    increase_and_square(ChangeMode::After, a);
}

In this example, we perform slightly different operation depending on the “mode” used. This is a fairly common behavior that can occur in may different forms.

However, implemented as it is there is not the most optimized way to do this. A best way to obtain the same result is to use the ChangeMode as a template instead of a plain parameter.

Like this :

#include <iostream>

// Enum describing the mode used in the following function
enum class ChangeMode
{
    Before,
    After
};

// Function that squares a value and increases it (wether before or after), then prints the result
template<ChangeMode m>
void increase_and_square(int v)
{
    if (m == ChangeMode::Before)
        ++v;
    
    v = v*v;
    
    if (m == ChangeMode::After)
        ++v;

    std::cout << v << std::endl; 
}

// main function
int main()
{
    const int a = 42;
    increase_and_square<ChangeMode::Before>(a);
    increase_and_square<ChangeMode::After>(a);
}

Impementing the function like is actually better in every way :

  • Operations done at compile time : of course, and as the main advantage of using templates, we evaluate more expressions a compile time here, saving run time.
  • Compiler optimization : since the compiler will be able to evaluate more things at compile time, it will be able to optimize the code, effectively discarding the if statements.
  • Executable size : more impressive, the executable will be smaller with the templated version. This is so because the fact that we discard the if statement actually greatly reduce the size of the functions.

To illustrate this, here is the template instanciation the compiler generates :

void increase_and_square<ChangeMode::Before>(int v)
{
    ++v;
    v = v*v;
    std::cout << v << std::endl; 
}

void increase_and_square<ChangeMode::After>(int v)
{
    v = v*v;
    ++v;
    std::cout << v << std::endl; 
}

These are way simpler functions that the big bulky one above.

If you are not convinced, here is the number of assembly instructions the compiler generates (I used Compiler Explorer (godbolt.org) with clang as compiler and --std=c++20 -O3 as compiler options).

Parameter

123

Template

76

The template version is clearly better, faster, prettier.

Perk : double default values

Using the template version of this code also provides another perk : the ability to give a default value to your mode and to your parameter.

Since default values are decorrelated for templates and parameters, you can write this :

#include <iostream>

// Enum describing the mode used in the following function
enum class ChangeMode
{
    Before,
    After
};

// Function that squares a value and increases it (wether before or after), then prints the result
template<ChangeMode m=ChangeMode::Before>
void increase_and_square(int v = 2)
{
    if (m == ChangeMode::Before)
        ++v;
    
    v = v*v;
    
    if (m == ChangeMode::After)
        ++v;

    std::cout << v << std::endl; 
}

int main()
{
    const int a = 42;
    increase_and_square(a);
    increase_and_square<ChangeMode::After>();
    increase_and_square();
}

This is interesting because the mode and the parameter actually have different semantic meanings, thus it is only logical that they have separate behavior.

Limitations

Why does this work ? Because the “mode” we template is actually an enum with very few values.

If you happen to use an bigger enum (with 5+ different values) or other types, the template version will be way worse than the parameter version.

In general, you will have no use doing what we saw in this article, but in the very specific case you have an enum that describes a “mode of use” of your function and has 2 or 3 possible values, take time to consider using it as template.

Keep in mind that this model, if seducing, is very specific and will not apply in your day-to-day coding.

Semantic

I will conclude this article by talking a bit about semantics.

What we achieved here could be seen as an overload of the increase_and_square() function. Indeed, we did several implementations of the same function (well, the template did it for us) that has a similar-yet-different behavior. This is exactly what overloading is about.

This is also why I described the enum as a “mode” : it describes how the function must work, and is more that than a real parameter.

Thanks for your attention and see you next week!

Author: Chloé Lourseyre

About &= and |=

Author: Chloé Lourseyre

I often happen to see, on pretty old projects, the operators &= and |= used to update a boolean indicating success that is returned along successive function calls. As in the following example :

bool success = true;
success &= DoOneThing();
success &= DoOneOtherThing();
success &= DoALastThing();
if (success)
    std::cout << "Success !" << std::endl;

This way of writting is more concise than the usual :

bool success = true;
success = success && DoOneThing();
success = success && DoOneOtherThing();
success = success && DoALastThing();
if (success)
    std::cout << "Success !" << std::endl;

But you should be cautious, because behind the operators &= and |= hide a few subtelties that one should be aware of.

Definition of the operators &= and |=

First, let’s see precise definition of &= and |=. In this section, I’ll only use &= as an example, but the behavious of |= is pretty much the same.

&= is an arithmetic binary operator, which permforms a binary AND and its assignation. In other terms, writting this :

v1 &= v2;

is equivalent to this :

v1 = v1 & v2;

except that v1 is only evaluated once.

For more insight about this, read the C++11 standard section 5.17 Assignment and compound assignment operators or go to https://en.cppreference.com/w/cpp/language/operator_arithmetic

What we call “arithmetic binary operators” (which use the symbols &, |, ^, << and >>) are operators that perform bit-to-bit operations.

For instance :

int a = 0b110;
int b = 0b011;
int c = a & b; // AND - 010
int d = a | b; // OR  - 111
int e = a ^ b; // XOR - 101

Writting a &= b; is thus performing a bit-to-bit AND operation on a and b, and assigns the result to a.

Differences between &= and &&, |= and ||

The operators && and || are, as for them, logical operators, as opposed to &= and |= who are arithmetic operators.

The use of &= and |= in a logical context is prone to diverge from what we could expect them to do.

Here are two example to illustrate that.

Example 1

#include <iostream>

bool foo()
{
    std::cout << "call foo()" << std::endl;
    return true;
}

int main() {
    bool c = true;
    std::cout << "Using |=" << std::endl;
    c |= foo();
    std::cout << "Using = and ||" << std::endl;
    c = c || foo();
    std::cout << "End" << std::endl;
    return 0;
}

The function foo() always returns true, as long as writing on the standard output to indicate it’s been called.

In the main() function, we create a bool c, initialized true, on which we will perform the arithmetic binary operator |= and the logical operator ||.

Here is the standard output :

Using |=
call foo()
Using = and ||
End

We can plainly see that in the |= case, foo() is called whereas it is not in the || case.

This is simply due to the fact that, in C++, logical operators are lazy. This means that if the right-hand-side of the operation can not change its result after evaluating the left-hand-side, then it won’t be evaluated.

Here, because c is already true in the expression c || foo(), no matter the value of foo() the result of the operation will always be true.

In the case of |=, it is a bit different. |= is not a logical operator, it does not reason in terms of true or false, but it performs arithmetic operations on integers. Thus, it needs to evaluate each side of the operation before evaluating the result. With arithmetic operations, both side are always evaluated.

Whether you wish the right-hand-side to be evaluated depends on the context. However, keep in mind that when C++ developpers see logical-like operations, they expect it to be lazy.

Example 2

The following example, though quite uncommon in practice, can lead to errors that are difficult to investigate.

#include <iostream>

int main() {
    int c1 = true;
    int c2 = 0b10;

    if (c1)
        std::cout << "c1 is true" <<std::endl;
    else
        std::cout << "c1 is false" <<std::endl;

    if (c2)
        std::cout << "c2 is true" <<std::endl;
    else
        std::cout << "c2 is false" <<std::endl;

    if (c1&&c2)
        std::cout << "c1&&c2 is true" <<std::endl;
    else
        std::cout << "c1&&c2 is false" <<std::endl;

    c1 &= c2;

    if (c1)
        std::cout << "c1&=c2 -> c1 is true" <<std::endl;
    else
        std::cout << "c1&=c2 -> c1 is false" <<std::endl;

    return 0;
}

Here, two variables are created : c1 which is initialized to true, and c2, which is initialized to 0b10.

Following four blocs performing logical evaluations :

  • First we logically evaluate c1.
  • Then, we similarly evaluate c2.
  • Then we evaluate the logical operation c1 && c2.
  • Finaly, we realise the arithmetic binary operation c1 &= c2 just before evaluating c1 again.

Here is the standard output of this code :

c1 is true
c2 is true
c1&&c2 is true
c1&=c2 -> c1 is false

Let’s see in details how this works :

First of all, c1 is considered true. This is expected because it is explicitly initiliazed with true.

Secondly, c2 is also considered true. This is so because, in C++, any non-null integer evaluates to true.

Thirdly, c1 && c2 is evaluated to true. Indeed, because c1 and c2 and individually true, performing the AND operation on them is logically true.

Fourthly, we perform c1 &= c2;. By doing so, we actually perform c1 & c2 and affect the result to c1.

However, according to the standard, true is equal to 1 when promoted to integer. Thus, c1 & c2 is equivalent to 0b01 & 0b10. The result to this operation really is 0b00, the bit-to-bit AND operation is false for every bit.

The 0 value being evaluated to false in a logical expression, c1 is, after all, false.

You won’t see this happen very often in live code, but there is plenty of situations, especially in legacy code, when an integer value that is not 1 is returned as if it was a true boolean.

Conclusion

The use of &= and |= will do what we expect in 99% of the case. But in the remaining 1%, you’ll have major bugs difficult to investigate.

Moreover, I’d like to pinpoint a semantic problem here. The fact that we ue an arithmetic operation instead of a logical one mislead the reader and forces them to do an additional effort to pull this straight. In addition to that, it may prevent the compiler to perform optimization.

Writting a = a && b; instead of a &= b; is not much an effort, with only a few additional characters, but it will protect you from bugs and hold better semantics.

Thank you for your attention and meeting you next week !

NB : The example were done with clang and the compiler options --std=c++20 -O3

Author: Chloé Lourseyre