What Are Unscoped Enumerations?

An unscoped enumeration is a program-defined type whose set of possible values is a fixed list of names that you write out yourself. Each name on that list is called an enumerator, and an object of the enumeration type may hold one of them and nothing else.

Here is the whole idea in one program. A bakery tracks where a batch of dough currently sits in its schedule:

#include <iostream>

enum DoughStage
{
    mixing,
    bulkRise,
    shaping,
    proofing,
    baking,
};

int main()
{
    DoughStage currentStage{ proofing };

    if (currentStage == proofing)
        std::cout << "Cover the tray and leave it alone." << '\n';
    else
        std::cout << "Check the schedule board." << '\n';

    return 0;
}

Output:

Cover the tray and leave it alone.

DoughStage is the enumeration. mixing, bulkRise, shaping, proofing, and baking are its enumerators. A DoughStage object can hold one of those five values, the compiler knows it, and the compiler enforces it.

What the enum Keyword Creates

One enum definition produces two separate things, and almost everything else in this lesson follows from keeping them apart.

A new type. DoughStage becomes a real type, as usable as int or bool: you can declare objects of it, pass it to functions, return it, and overload on it. It is a distinct type, so the compiler will not silently accept a value that came from somewhere else.

A set of names. Each enumerator becomes a named constant of that type. Enumerators are implicitly constexpr, so they are usable anywhere a compile-time constant is required. The word unscoped describes where those names are put, which the section on scope covers below.

The syntax itself is short:

  • the keyword enum, then the name of the type
  • the enumerators inside braces, separated by commas (commas, never semicolons)
  • a trailing comma after the last enumerator, which is optional but keeps later edits to one line
  • a semicolon after the closing brace, because this is a type definition

One enumerator per line is the usual layout. A handful of short names with no comments can share a line.

Nomenclature
The enumeration (or enumerated type, or enum) is the type: DoughStage. An enumerator is one named value belonging to it: proofing. Since an enumeration is a program-defined type, the compiler needs its full definition before you can use it. A forward declaration is not enough.

Why Not Just Use an Integer?

A fixed list of states can be modelled without enumerations at all, and it is worth seeing exactly how far each alternative gets you.

Approach Invalid value rejected Own type in error messages Names tied to the concept
int holding 0, 1, 2 No No No
int plus constexpr named constants No No Loosely, by convention
Type alias plus constexpr named constants No No Yes, by name
Enumeration Yes Yes Yes, by the type system

The third row is the one that fools people, because it reads like a type. The following program compiles cleanly and is still wrong:

#include <iostream>

using LoafShape = int; // an alias for int, and only an alias

constexpr LoafShape boule{ 0 };
constexpr LoafShape batard{ 1 };
constexpr LoafShape baguette{ 2 };

int main()
{
    LoafShape onTheBench{ baguette }; // fine
    onTheBench = 41;                  // also fine, though 41 names no shape

    std::cout << "Shape code on the bench: " << onTheBench << '\n';

    return 0;
}

Output:

Shape code on the bench: 41

A type alias is another spelling for int, so onTheBench really is an int and 41 really is a legal value for it. The name documents intent; it does not defend it. There is a debugging cost too, since a watch window shows you 41 rather than any hint of what shape that was meant to be.

An enumeration closes the hole. DoughStage is not a spelling of int, so a value that is not one of its enumerators has no way in.

Every Enumeration Is Its Own Type

Because each enumeration is a distinct type, enumerators belonging to one of them cannot be used to initialize another, even when both enumerations sit side by side in the same file.

The following program is broken on purpose:

enum DoughStage
{
    mixing,
    bulkRise,
    shaping,
};

enum LoafShape
{
    boule,
    batard,
    baguette,
};

int main()
{
    DoughStage currentStage{ batard }; // error: batard belongs to LoafShape

    return 0;
}

The compiler reports:

s.cpp: In function 'int main()':
s.cpp:17:30: error: cannot convert 'LoafShape' to 'DoughStage' in initialization
   17 |     DoughStage currentStage{ batard }; // error: batard belongs to LoafShape
      |                              ^~~~~~
      |                              |
      |                              LoafShape

Notice what the diagnostic knows. It names both types and points at the exact initializer. Neither of the integer-based approaches above could have produced that message, because to a compiler they were all just int.

Where the Enumerator Names Live

Now the unscoped part. An unscoped enumeration puts its enumerator names into the scope that encloses the enumeration definition, rather than keeping them inside the enumeration. Define DoughStage at global scope and mixing, bulkRise, shaping, proofing, and baking are all global names.

The enumeration does also act as a named scope region for its own enumerators, so both spellings below refer to the same thing:

#include <iostream>

enum DoughStage
{
    mixing,
    bulkRise,
    shaping,
    proofing,
    baking,
};

int main()
{
    DoughStage currentStage{ shaping };           // enumerator named directly
    DoughStage nextStage{ DoughStage::proofing }; // the same name, qualified

    if (currentStage != nextStage)
        std::cout << "The dough has a stage still to go." << '\n';

    return 0;
}

Output:

The dough has a stage still to go.

Both forms are valid, and unqualified is what most code uses. Note that the type name is never repeated when declaring an object: it is DoughStage currentStage{ shaping };, not enum DoughStage currentStage{ shaping };.

The price of unqualified names is that generic words leak into the surrounding scope and can only be claimed once. This program is broken on purpose:

enum MixerSpeed
{
    stop,
    fold,
    whip,
};

enum OvenFan
{
    stop, // error: the name stop is already taken in this scope
    gentle,
    full,
};

int main()
{
    return 0;
}

The compiler reports:

s.cpp:10:5: error: 'stop' conflicts with a previous declaration
   10 |     stop, // error: the name stop is already taken in this scope
      |     ^~~~
s.cpp:3:5: note: previous declaration 'MixerSpeed stop'
    3 |     stop,
      |     ^~~~

The two enumerations are unrelated and neither is at fault on its own. They collide only because both dropped a name called stop into the same scope.

Giving the Names a Home

Collisions are a scoping problem, so the fix is to choose a scope deliberately instead of defaulting to the global one.

Situation Where to define the enumeration
Used only inside one function Inside that function
Used throughout one source file At file scope in that file
Used by several files In a header, wrapped in a namespace
Enumerator names are generic words In a namespace, or prefix the names

The weakest fix is to lengthen the names by hand, prefixing each enumerator with something derived from its enumeration:

#include <iostream>

enum MixerSpeed
{
    mixer_stop,
    mixer_fold,
    mixer_whip,
};

enum OvenFan
{
    fan_stop, // no collision with mixer_stop
    fan_gentle,
    fan_full,
};

int main()
{
    MixerSpeed paddle{ mixer_fold };
    OvenFan airflow{ fan_stop };

    if (paddle == mixer_fold && airflow == fan_stop)
        std::cout << "Folding slowly with the fan off." << '\n';

    return 0;
}

Output:

Folding slowly with the fan off.

This works, and you will meet it often in existing code, but the names are still global. It lowers the odds of a clash rather than removing the possibility.

The better fix is a namespace, which gives the enumerators a scope region of their own and leaves the plain words intact:

#include <iostream>

namespace mixer
{
    enum Speed
    {
        stop,
        fold,
        whip,
    };
}

namespace oven
{
    enum Fan
    {
        stop, // oven::stop is a different name from mixer::stop
        gentle,
        full,
    };
}

int main()
{
    mixer::Speed paddle{ mixer::fold };
    oven::Fan airflow{ oven::stop };

    if (paddle == mixer::fold && airflow == oven::stop)
        std::cout << "Two settings, plain names, no clash." << '\n';

    return 0;
}

Output:

Two settings, plain names, no clash.

If nothing outside a single function ever refers to the enumeration, define it in that function. Its names then live in the function's scope, and they shadow any identically named enumerators from an outer scope:

#include <iostream>

void reportBench()
{
    // Nothing outside this function needs these names
    enum BenchState
    {
        swept,
        floured,
        crowded,
    };

    BenchState bench{ crowded };

    if (bench == crowded)
        std::cout << "Clear the bench before shaping." << '\n';
}

int main()
{
    reportBench();

    return 0;
}

Output:

Clear the bench before shaping.
Best Practice
Give every enumeration the narrowest scope that its users allow. Prefer a namespace (or a class) over the global scope, and prefer a function body over both when only that function needs the type.
Related Content
A class is also a scope region, and enumerations belonging to a class are commonly defined inside it. Scoped enumerations, covered shortly, build the scope region into the enumeration itself so no namespace is needed.

Naming Enumerations and Enumerators

Enumerated types follow the convention for all program-defined types and start with a capital letter. Enumerators have no single agreed convention, so pick one and hold to it across a codebase.

Best Practice
Start enumeration type names with a capital letter (DoughStage) and enumerator names with a lower-case letter (proofing).
Warning
Avoid all-caps enumerators such as PROOFING. All caps is the convention for preprocessor macros, and a macro of the same name will rewrite your enumerator out of the code before the compiler ever sees it. Avoid leading capitals for enumerators as well, since that spelling suggests a type. Enumerations may legally be left unnamed, but an unnamed enumeration gives you no type to declare objects with, so name every one you write.

Comparing and Branching

Two enumeration objects of the same type compare with operator== and operator!=, which is the everyday way to ask what state something is in. The full set of relational operators works too, and the ordering follows the order in which you declared the enumerators. That makes a naturally sequential enumeration, such as a schedule, answerable with a single comparison:

#include <iostream>

enum DoughStage
{
    mixing,
    bulkRise,
    shaping,
    proofing,
    baking,
};

int main()
{
    DoughStage currentStage{ proofing };

    if (currentStage >= shaping)
        std::cout << "The bench work is done." << '\n';

    if (currentStage < baking)
        std::cout << "The oven is still free." << '\n';

    return 0;
}

Output:

The bench work is done.
The oven is still free.

Comparisons work because every enumerator has an integral value behind its name. The next lesson looks at those values directly, along with the conversions they enable.

When a function has several outcomes to report, returning an enumeration beats returning a numeric code, and the caller can dispatch on it with a switch. Listing every enumerator as a case also gets you a compiler warning later if someone adds a new one:

#include <iostream>

enum TrayResult
{
    seated,
    doorAjar,
    rackFull,
};

TrayResult loadOven(int freeRacks, bool doorClosed)
{
    if (!doorClosed)
        return doorAjar;

    if (freeRacks == 0)
        return rackFull;

    return seated;
}

int main()
{
    switch (loadOven(0, true))
    {
        case seated:
            std::cout << "Tray in. Set the timer." << '\n';
            break;
        case doorAjar:
            std::cout << "Push the door shut and try again." << '\n';
            break;
        case rackFull:
            std::cout << "Every rack is taken." << '\n';
            break;
    }

    return 0;
}

Output:

Every rack is taken.

Enumerations are small and cheap to copy, so pass and return them by value as this example does.

Summary

An enumeration is a program-defined type whose values are a fixed list of named constants called enumerators. Unscoped ones are defined with the enum keyword, hold their enumerators in braces separated by commas, and end in a semicolon.

Each enumeration is a distinct type, unlike a type alias, so the compiler rejects a value that did not come from that enumeration and names both types when it does.

Enumerator names go into the enclosing scope, not into the enumeration, which is what unscoped means. The enumeration is also a scope region, so DoughStage::proofing and a bare proofing both work.

Two enumerations in one scope cannot share an enumerator name. The compile error is a naming collision, not a problem with either enumeration on its own.

Fix collisions by choosing a scope: prefix the enumerators, or better, put the enumeration in a namespace or class, or inside the single function that uses it.

Name the type with a leading capital and the enumerators in lower case. Avoid all-caps enumerators, which collide with macro conventions.

Compare with == and != to test which enumerator an object holds, and with the relational operators when the enumerators were declared in a meaningful order. Returning an enumeration and dispatching with a switch is the readable alternative to numeric status codes.