C++ enums

C++ enums

C++ enum is a user-defined type that allows us to give textual names for integer values. Using enums you can increase code readability and reduce the probability of faults due to usage of hardcoded values. I will talk here about C++ different enumerations types and usages. Note that at least C++ 11 is required.

In C++, there are two types of enums

  • Scoped enums
  • Unscoped enums

Unscoped enums - called also plain enums - are very similar to the C language enums. We will start with them illustrating their C like usage, then introduce the new features added to them on C++.

Unscoped enums

You can use enums in C++ the same way as in C. The following code creates an enum that represents the traffic sign colors

// Creating an enum for the traffic sign light colors
enum trafficSignColors {
    red,
    yellow,
    green,
};

Enumerators have an integer type that can at least hold it's enumerators' greatest value. By default the first enumerator will have the value of 0 and the value will be increased by 1 for each subsequent enumerator. You can change the start value or even assign all enumerators' values explicitly.

// Creating an enum for the traffic sign light colors
enum trafficSignColors {
    red = 10,   // Initial value of 10
    yellow,     // yellow = 11 (initial + 1)
    green = 5,  // Values don't need to be in order
};

Using enums you can avoid usage of hard-codded values that represent the traffic sign colors. You can use preprocessing defines to achieve this too, but enums can enhance code readability by creating a type that represents the traffic sign state.

typedef enum {
    red,
    yellow,
    green,
} trafficSignState;

You can now use the new type to create your variables, as a function input parameter type or return value type.

/* A function to print the name of the traffic sign state */
void printTrafficSignState(trafficSignState tsColor) {
    switch(tsColor) {
        case red:
            cout << "Traffic sign red state" << endl; 
            break;
        case yellow:
            cout << "Traffic sign yellow state" << endl; 
            break;
        case green:
            cout << "Traffic sign green state" << endl; 
            break;
        default:
            cout << "Traffic sign unknown state" << endl; 
    }
}

This type of enum is called unscoped because the enumerators - named constants - are part of their enum scope - the enum keyword is not creating a new sub-scope - which means that we can't have two enumerators with the same name even if they belong to different enums.

// Creating an enum for the traffic sign light colors
enum trafficSignColors {
    red,
    yellow,
    green,
};

// Creating an enum for the colors
enum colors {
    red, // Invalid, red is already defined in this scope
    black,
    white,
}

Here the usage of red in the colors enum is invalid as red is already part of the current scope after being defined in the trafficSignState.

Plain enums have a default implicit conversion to an integer, but the reverse is not true and you need to use an explicit cast. The following code re-implement the printTrafficSignState function by using an integer value as a parameter instead of the trafficSignState type

enum trafficSignColors {
    red,
    yellow,
    green,
};

/* A function to print the name of the traffic sign state */
void printTrafficSignState(int tsColor) {
    switch(tsColor) {
        case red:
            cout << "Traffic sign red state" << endl; 
            break;
        case yellow:
            cout << "Traffic sign yellow state" << endl; 
            break;
        case green:
            cout << "Traffic sign green state" << endl; 
            break;
        default:
            cout << "Traffic sign unknown state" << endl; 
    }
}

int main() {
    int tsColor = green;

    printTrafficSignState(tsColor);

    return(0);
}

Imagine the case that you have some class/structure object with one of its fields of some enum type, and you are transferring this object in binary format between two apps that are built with different compilers or for different architectures. The problem here is that the object can have different size and internal offsets in the two apps, which will lead to the received binary data to be loaded in the wrong format. C++ provides a solution for this by letting you determine the underlying type of the enumerators as in this line below typedef enum: int.

/* We can determine the base type of the enum. This is new to C++.
   We can't use red, yellow and green as they are already used */
typedef enum : int {
    red_t,
    yellow_t,
    green_t,
} trafficSignState_t;

Another C++ feature that you can use with plain enums is the C++ operator overloading. This can make programmer life easier, make the code cleaner, and eliminate the need for dealing with enumerators as integer values.

/* Overloading the prefix ++ operator for the trafficSignState enum */
trafficSignState &operator++(trafficSignState& orig) {
    /* Enum can be implicitally converted to integers, integers needs
       to be explicitally converted to enums */
    orig = static_cast<trafficSignState>(orig + 1);

    if(orig > green) {
        orig = red;
    }

    return(orig);
}

Check this complete example that shows the main usage of plain enums

#include <iostream>
#include <string>

using namespace std;

/* Define the traffic sign enum as a type. This is the same way
   we can create an enum in C */
typedef enum {
    red,
    yellow,
    green,
} trafficSignState;

/* Overloading the prefix ++ operator for the trafficSignState enum */
trafficSignState &operator++(trafficSignState& orig) {
    /* Enum can be implicitally converted to integers, integers needs
       to be explicitally converted to enums */
    orig = static_cast<trafficSignState>(orig + 1);

    if(orig > green) {
        orig = red;
    }

    return(orig);
}

/* We can determine the base type of the enum. This is new to C++.
   We can't use red, yellow and green as they are already used */
typedef enum : int {
    red_t,
    yellow_t,
    green_t,
} trafficSignState_t;

/* Override the stream operator */
ostream &operator<<(ostream &output, const trafficSignState_t &state) {
    string stateStr;

    switch(state) {
        case red:
            stateStr = "red"; 
            break;
        case yellow:
            stateStr = "yellow"; 
            break;
        case green:
            stateStr = "green"; 
            break;
        default:
            stateStr = "invalid"; 
    }

    output << stateStr;

    return output;            
}

/* A function to print the name of the traffic sign state */
void printTrafficSignState(trafficSignState tsColor) {
    switch(tsColor) {
        case red:
            cout << "Traffic sign red state" << endl; 
            break;
        case yellow:
            cout << "Traffic sign yellow state" << endl; 
            break;
        case green:
            cout << "Traffic sign green state" << endl; 
            break;
        default:
            cout << "Traffic sign unknown state" << endl; 
    }
}

int main()
{
    /* Create a variable to hold the traffic sign state */
    trafficSignState tsState = green;

    /* Pass the value to be printed */
    printTrafficSignState(tsState);

    /* Unscoped enums can be implicitily converted to integer types */
    cout << "Traffic sign state value: " << tsState << endl;
    int stateAsInteger = ++tsState;
    cout << "Traffic sign state value again: " << stateAsInteger << endl;

    /* Using stram overloaded operator */
    trafficSignState_t tsState_2 = green_t;
    cout << "Final traffic sign state: " << tsState_2 << endl;

    return 0;
}

Scoped enums

In the plain enum examples above, we can not use the same name for enumerators from different enums as all of them are on the same scope. Scoped enum solves this problem by adding the enumerators to a new scope that needs to be accessed using the enum names. The only difference between scoped and unscoped enum declaration is that you need to use either the class or struct keyword after the enum keyword.

/* Scoped enum, we can use either enum class or enum struct */
enum class TraficSignState {
    red,
    yellow,
    green,
};

/* red, yellow and green can be used again normally */
enum class colors: int {
    red,
    black,
    white,
}

Note that the red enumerator name is used in both enums without a collision. Scoped enums also eliminate the need to use typedef to create a new type as the scoped enum can be used directly as a function parameter or return type.

void printTrafficSignState(TraficSignState tsColor) {
    switch(tsColor) {
        /* Enum name should be used to access enum value */
        case TraficSignState::red:
            cout << "Traffic sign red state" << endl; 
            break;
        case TraficSignState::yellow:
            cout << "Traffic sign yellow state" << endl; 
            break;
        case TraficSignState::green:
            cout << "Traffic sign green state" << endl; 
            break;
        default:
            cout << "Traffic sign unknown state" << endl; 
    }
}

Unlike plain enums, scoped enums don't have implicit conversions to integers and you need to use an explicit cast for the conversion. This provides type safety and makes our code less error-prone. Check the following code for a complete example of scoped enums

#include <iostream>

using namespace std;

/* Scoped enum, we can use either enum class or enum struct */
enum class TraficSignState {
    red,
    yellow,
    green,
};

/* red, yellow and green can be used again normally */
enum class TraficSignState_t : int {
    red,
    yellow,
    green,
};

void printTrafficSignState(TraficSignState tsColor) {
    switch(tsColor) {
        /* Enum name should be used to access enum value */
        case TraficSignState::red:
            cout << "Traffic sign red state" << endl; 
            break;
        case TraficSignState::yellow:
            cout << "Traffic sign yellow state" << endl; 
            break;
        case TraficSignState::green:
            cout << "Traffic sign green state" << endl; 
            break;
        default:
            cout << "Traffic sign unknown state" << endl; 
    }
}

int main()
{
    /* Create a variable to hold the traffic sign state */
    TraficSignState tsState = TraficSignState::yellow;

    /* Pass the value to be printed */
    printTrafficSignState(tsState);

    /* Scoped enums need an explicit cast and will not compile without it */
    cout << "Traffic sign state value: " << static_cast<int>(tsState) << endl;

    TraficSignState_t tsState_2 = TraficSignState_t::yellow;

    return 0;
}

To conclude, enum is a very useful user-defined type that both C and C++ provide for us. Knowing how to use it correctly and what C++ add to it can enhance our code readability and help reducing errors too. You can use the plain enums in situations that need only a C like enum, but you can get more code readability, type safety and avoid name collisions by using scoped enums.

References

  • The C++ Programming Language (4th edition), by Bjarne Stroustrup
  • A Tour of C++ (2nd edition), by Bjarne Stroustrup
  • cppreference.com

Comments

  1. The article is very organized,has simple words and examples.
    Thank you for adding new features in c++ which I learned form this article.

    ReplyDelete
    Replies
    1. Glad to know that you get new info from this post 🙂

      Delete
  2. Very informative, well written article. Thank you for sharing this information.

    ReplyDelete
  3. The article is well detailed and easy to understand.
    Keep up the good work.

    ReplyDelete

Post a Comment

Popular posts from this blog

Basic TCP analysis with Wireshark - Part 1

Data transmission over TCP