Baduit

A young French developper who really likes (modern) C++

About me
08 March 2023

Operator overload for enumerations

by Baduit

Article::Article

You probably all have seen a snippet of C++ code looking like this if you ever had a course or seen a tutorial about operator overload in C++:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>

struct Person
{
    std::string first_name;
    std::string last_name;
};

std::ostream& operator<<(std::ostream& os, const Person& p)
{
    return (os << "First name: " << p.first_name << " Last name: " << p.last_name);
}

int main()
{
    Person p{ "Jane", "Doe" };
    std::cout << p << std::endl;
    // The output is: "First name: Jane Last name: Doe"
}

Compiler explorer link

You can note that here the overload of the operator << is a free function and not a member function.

The point?

If it is a possible to do it with free functions, maybe it could also work for other user defined types like enumerations.

Let’s see what cppreference (the reference if you have to look up things about C++, yes, I know it is in the name): When an operator appears in an expression, and at least one of its operands has a class type or an enumeration type, then overload resolution is used to determine the user-defined function to be called

My intuition was correct and some of you probably already knew it.

Some examples

The classic

You want to log your enum but you don’t use a library like Magic Enum (try it, it is awesome) then you write this type of stuff:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <utility>

enum class ToggleStatus
{
    ON,
    OFF
};

std::ostream& operator<<(std::ostream& os, ToggleStatus status)
{
    if (status == ToggleStatus::ON)
        os << "ON";
    else
        os << "FALSE";
    return os;
}

Compiler explorer link

The chaotic evil

Are you tired of playing a horny bard in D&D? Let’s play a chaotic evil developer and watch the world burn!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
enum class Int
{
    ONE, // 0
    TWO, // 1
    THREE // 2
};

constexpr Int operator+(const Int& a, const Int& b) {
    if (a == Int::ONE)
        return Int::ONE;
    else if (b == Int::ONE)
        return Int::TWO; 
    else if (a == b)
        return Int::THREE; 
    else
        return Int::ONE; 
}

static_assert((Int::ONE + Int::TWO) == Int::ONE);
static_assert((Int::TWO + Int::ONE) == Int::TWO);
static_assert((Int::TWO + Int::TWO) == Int::THREE);
static_assert((Int::THREE + Int::THREE) == Int::THREE);
static_assert((Int::TWO + Int::THREE) == Int::ONE);

Compiler explorer link

Meme using the template go brrrrr and virgin is saying we can't overload operators like this but overload still go brrrrrr

The weird but funny

If you are still not using Magic Enum but you want to iterate over the values of an enumeration, here’s an example of how to do it by using your enumeration like an iterator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <iostream>
#include <utility>

enum class UserStoryStatus
{
    TO_REFINE, // 0
    REFINED, // 1
    TO_DO, // 2
    WORK_IN_PROGRESS, // 3
    TO_VALIDE, // 4
    DONE, // 5
    // Using out of range value as a sentinel would be UB
    // Therefore, I need to define it
    END 
};

// Pre increment operator
// Note: this function could be a one liner with 0 copy, but I detailed it for learning purpose
UserStoryStatus& operator++(UserStoryStatus& uss)
{
    // Use std::to_underlying to get the actual integer representation
    auto i = std::to_underlying(uss);
    // All values are continuous because we did not specify them
    // It means we can just increase by one the integer representation of the enum
    i += 1;
    // The end should be checked using the UserStoryStatus::END sentinel value
    // So I wont't check we are still in the right range
    // Because this should be done with the end function
    // But feel free to throw and exception if you want to do that check
    // Or keep returning UserStoryStatus::END
    uss = static_cast<UserStoryStatus>(i);
    return uss;
}

// Post increment operator
UserStoryStatus operator++(UserStoryStatus& uss, int)
{
    return ++uss;
}

// Dummy dereference operator, because iterator are supposed to be dereferenceable
UserStoryStatus operator*(UserStoryStatus uss)
{
    return uss;
}

// Dummy type to use a the range
struct UserStoryStatusRange{};

// Begin and end function so it act as a range
UserStoryStatus begin(UserStoryStatusRange status)
{
    return UserStoryStatus::TO_REFINE;
}

UserStoryStatus end(UserStoryStatusRange status)
{
    return UserStoryStatus::END;
}


int main(int, char **)
{
    UserStoryStatusRange range;
    for (auto s: range)
        std::cout << std::to_underlying(s) << std::endl;
}

Compiler explorer link

Article::~Article

I hope you enjoyed this article or even better you learned something. With a little bit of (bad)luck it created a vocation for someone to be a chaotic evil developer.

Sources

tags: cpp