避免重复复制粘贴 static_cast<size_t>(enum_type) 以将枚举 class 转换为其基础类型

Avoiding repetitive copy-paste of static_cast<size_t>(enum_type) for casting an enum class to its underlying type

我的代码中有一个枚举类型,如下所示:

enum class GameConsts: size_t { NUM_GHOSTS = 4 };

我发现自己重复了所需的 static_cast 以获得 enum 值:

Ghost ghosts[static_cast<size_t>(GameConsts::NUM_GHOSTS)];
// and...
for(size_t i = 0; i < static_cast<size_t>(GameConsts::NUM_GHOSTS); ++i) { ... }

避免这种重复的最佳方法是什么static_cast

在此 ISO proposal discussion 中提出了允许实施转换运算符的选项,但似乎已放弃。

相关的 SO 问题:


Davis Herring 在 C++23 添加的评论中添加:std::to_underlying 当我们有 C++23 编译器支持时,这应该是答案。但问题是针对 C++20 的。

您的第一个选择是使用 constexpr 而不是枚举:

constexpr size_t NUM_GHOSTS = 4;

你可以把它放在适当的上下文中,比如 GameConsts struct:

struct GameConsts {
    static constexpr size_t NUM_GHOSTS = 4;
};

那就不用选角了:

Ghost ghosts[GameConsts::NUM_GHOSTS];

如果您确实需要一个枚举,因为您有一个要一起管理的相关值列表,那么您可以使用无范围的枚举(即从 enum class 中删除 class并使用常规的普通旧 enum) 但将其放在结构中以保留上下文。我将在这里使用另一个枚举的示例,管理几个相关值。

enum class GameKeys: char { UP = 'W', RIGHT = 'D', DOWN = 'X', LEFT = 'A' };

上述枚举可能会重复使用 static_cast,在这样的 switch-case 中:

char key_pressed;
// ...
switch(key_pressed) {
    case static_cast<char>(GameKeys::UP): // ...
        break;
    case static_cast<char>(GameKeys::RIGHT): // ...
        break;
    case static_cast<char>(GameKeys::DOWN): // ...
        break;
    case static_cast<char>(GameKeys::LEFT): // ...
        break;
}

为避免重复需要 static_cast,您可以选择:


选项 1:在结构中使用简单的无作用域枚举

struct GameKeys {
    enum: char { UP = 'W', RIGHT = 'D', DOWN = 'X', LEFT = 'A' };
};

并且由于旧式枚举可以隐式转换为其基础类型,因此您可以摆脱转换:

switch(key_pressed) {
    case GameKeys::UP: // ...
        break;
    case GameKeys::RIGHT: // ...
        break;
    case GameKeys::DOWN: // ...
        break;
    case GameKeys::LEFT: // ...
        break;
}

选项 2:添加您自己的转换函数

如果您真的喜欢,或者必须使用 enum class,您可能有一个简单的转换函数,复制粘贴只是调用该函数,比完整的 static_cast 语法更简单:

enum class GameKeys: char { UP = 'W', RIGHT = 'D', DOWN = 'X', LEFT = 'A' };

// a simple "val" function - specific for our GameKeys
constexpr char val(GameKeys key) { return static_cast<char>(key); }

并且:

switch(key_pressed) {
    case val(GameKeys::UP): // ...
        break;
    case val(GameKeys::RIGHT): // ...
        break;
    case val(GameKeys::DOWN): // ...
        break;
    case val(GameKeys::LEFT): // ...
        break;
}

如果选择最后一个选项,您可能希望将其概括为任何类型的 enum,其中 this code:

// creating a "concept" for enums
template<typename E>
concept EnumType = std::is_enum_v<E>;

// creating a generic "val" function for getting the underlying_type value of an enum
template<EnumType T>
constexpr auto val(T value) {
    return static_cast<std::underlying_type_t<T>>(value);
}

选项 3:投射到枚举而不是来自枚举

根据@Nathan Pierson and @apple apple in the comments, the casting can be to the enum, with this code的建议:

char key_pressed = 'E';
// cast to the enum
GameKeys key = static_cast<GameKeys>(key_pressed);
switch(key) {
    case GameKeys::UP: // ...
        break;
    case GameKeys::RIGHT: // ...
        break;
    case GameKeys::DOWN: // ...
        break;
    case GameKeys::LEFT: // ...
        break;
    default: // ignore any other keys
        break;
}

即使 key_pressed 不是任何枚举值,这也应该可以正常工作,因为我们有一个固定的枚举(具有规定的基础类型,请注意枚举 class 始终是固定的,即使没有明确说明)。另见:What happens if you static_cast invalid value to enum class?

如果您经常将作用域枚举转换为整数...您实际上不需要作用域枚举。你似乎想要的是一堆 static constexpr 变量在他们自己的命名空间中:

struct GameConsts
{
  static constexpr size_t NumGhosts = 4;
  ...
};

您似乎不需要作用域枚举。随便写

namespace GameConsts {
    constexpr std::size_t NUM_GHOSTS = 4;
    // Add 'inline' if you can use C++17 or later modes and
    // don't want the declaration to result in multiple objects.
}

struct GameConsts {
    static constexpr std::size_t NUM_GHOSTS = 4;
};

替换std::to_underlying

C++20

在 C++20 中你可以只写

template<class E>
constexpr std::underlying_type_t<E> to_underlying(E e) noexcept
{
    return static_cast<std::underlying_type_t<E>>(e);
}

此实现受到很好的约束,因为自 C++20 起 std::underlying_type 需要对 SFINAE 友好。

C++11

如果您想将实现反向移植到 C++11(第一个具有作用域枚举的 C++ 标准修订版),也许您应该编写此代码以避免 std::underlying_type.

中的未定义行为
template<class T, bool = std::is_enum<T>::value>
struct aux_underlying_type {};
template<class E>
struct aux_underlying_type<E, true> {
    static_assert(bool(sizeof(E)), "The template parameter must be a complete enumeration type.");
    using type = typename std::underlying_type<E>::type;
};

template<class E>
using aux_underlying_type_t = typename aux_underlying_type<E>::type;

template<class E>
constexpr aux_underlying_type_t<E> to_underlying(E e) noexcept
{
    return static_cast<aux_underlying_type_t<E>>(e);
}