Using Enum in Modern C++

In modern C++, the using enum declaration introduced in C++20 provides an elegant mechanism to import scoped enumerators directly into a local scope. While scoped enumerations (enum class) introduced in C++11 eliminated namespace pollution and implicit type conversions, they often introduced verbose and repetitive syntax. The using enum feature preserves the underlying strong typing of scoped enumerations while allowing developers to drop repetitive type prefixes within specific code blocks, improving readability without sacrificing safety.

The Problem with Scoped Enumerations

Prior to C++20, developers working with enum class were required to fully qualify every enumerator with its enumeration name:

enum class Status {
    Pending,
    Approved,
    Rejected
};

void process(Status s) {
    if (s == Status::Pending) {
        // ...
    }
}

This explicit qualification prevented name collisions in global and namespace scopes. However, inside control structures—most notably switch statements—repeating the type prefix adds unnecessary visual clutter:

switch (s) {
    case Status::Pending:  break;
    case Status::Approved: break;
    case Status::Rejected: break;
}

How using enum Imports Enumerators

The using enum declaration injects all enumerators of a designated enumeration into the declarative region where the statement appears. The syntax is straightforward:

using enum TypeName;

When this statement is placed inside a local block (such as a function body, a loop, or a switch block), the enumerators become unqualified identifiers within that block only. Once execution leaves that scope, the unqualified names are no longer visible, preventing namespace pollution.

void handleStatus(Status s) {
    switch (s) {
        using enum Status; // Introduces Pending, Approved, Rejected into this switch block
        case Pending:
            // Handled without qualification
            break;
        case Approved:
            // Handled without qualification
            break;
        case Rejected:
            // Handled without qualification
            break;
    }
}

Scope Boundaries and Lifetime

The visibility of imported enumerators strictly respects C++ scope resolution rules:

  • Block Scope: Declaring using enum inside curly braces ({}) confines the unqualified names exclusively to that block and any nested blocks.
  • Class Scope: Declaring using enum within a class definition introduces the enumerators as members of that class, accessible to member functions without qualification and to outside code via the class name.
  • Namespace Scope: Placing the declaration at the namespace level makes the enumerators available throughout that namespace. While valid, this is generally discouraged in header files to prevent unexpected identifier collisions.

Importing Individual Enumerators

In addition to importing an entire enumeration with using enum, modern C++ also permits importing individual enumerators using standard using syntax:

void evaluate(Status s) {
    using Status::Approved; // Only 'Approved' is introduced into local scope
    
    if (s == Approved) {
        // 'Approved' can be used without qualification
    } else if (s == Status::Pending) {
        // 'Pending' still requires qualification
    }
}

Safety and Collision Handling

The using enum declaration does not convert a scoped enumeration into an unscoped one; the underlying type remains strongly typed. The compiler will still disallow implicit conversions to integers or comparisons between distinct enumeration types.

If an imported enumerator shares an identifier with an existing local variable or function within the same scope, the compiler issues a redeclaration error. If the conflicting identifier exists in an outer scope, the newly imported enumerator shadows the outer identifier within that local scope, adhering to standard C++ name lookup behavior.