modernize-use-to-underlying#

Finds casts from a scoped enumeration (enum class) to an integer type and replaces them with a call to std::to_underlying (introduced in C++23).

Converting a scoped enumeration to a hard-coded integer type is error-prone: if the enumeration’s underlying type is later changed, every such cast silently becomes a narrowing, widening or sign-changing conversion. std::to_underlying always yields exactly the underlying type and keeps the code correct.

Example:

enum class Color : unsigned char { Red, Green, Blue };

void f(Color c) {
  // Before:
  auto value = static_cast<unsigned char>(c);
  // After:
  auto value = std::to_underlying(c);
}

The check matches static_cast, C-style casts and functional-style casts.

Precise and imprecise casts#

A cast is precise when its destination type is exactly the underlying type of the enumeration. A cast is imprecise when the destination type is an integer type other than the underlying type (a different width or signedness).

enum class E : int {};

// precise cast
int i = static_cast<int>(E{});
// imprecise cast
unsigned j = static_cast<unsigned>(E{});

Precise casts are always rewritten.

Imprecise casts perform an additional integer conversion, so there is no single correct rewrite. Rewrites of imprecise casts are controlled by the ImpreciseCasts option.

A cast to a non-integer type (floating point, pointer, another enumeration) is never flagged. A cast to bool is only flagged when bool is the exact underlying type of the enumeration; otherwise it is treated as a truthiness test and left untouched.

Options#

ImpreciseCasts#

Controls how imprecise casts (whose destination type differs from the underlying type) are handled. Precise casts are always diagnosed and fixed regardless of this option. Possible values:

  • Ignore: Do not diagnose imprecise casts.

  • Warn: Diagnose imprecise casts but do not offer a fix-it. Neither automatic rewrite is applied because both change the meaning of the code in ways that may not be intended.

  • PreserveType: Wrap the operand in a call to the replacement function, keeping the original destination type:

    long l = static_cast<long>(E{});
    // becomes:
    long l = static_cast<long>(std::to_underlying(E{}));
    
  • UseUnderlyingType: Replace the whole cast with a call to the replacement function. This changes the type of the expression from the destination type to the underlying type, so use it only when the wider or differently-signed destination type was itself unintended:

    long l = static_cast<long>(E{});
    // becomes:
    long l = std::to_underlying(E{});
    

Default is Warn.

ReplacementFunction#

The fully qualified name of the function used in the replacement. Set this to use a hand-rolled equivalent (for example llvm::to_underlying) when targeting a language standard before C++23. When the value is std::to_underlying, the check only runs in C++23 or later; with any other value it runs from C++11 onwards. Default is std::to_underlying.

ReplacementFunctionHeader#

The header to include when the replacement function is used. When the value is enclosed in angle brackets the include directive uses angle brackets, otherwise it uses quotes. Default is <utility> when ReplacementFunction is set to std::to_underlying, and otherwise empty (no include is added).

IncludeStyle#

A string specifying which include-style is used, llvm or google. Default is llvm.