如何将强类型枚举自动转换为整数?

249
#include <iostream>

struct a {
  enum LOCAL_A { A1, A2 };
};
enum class b { B1, B2 };

int foo(int input) { return input; }

int main(void) {
  std::cout << foo(a::A1) << std::endl;
  std::cout << foo(static_cast<int>(b::B2)) << std::endl;
}

a::LOCAL_A 是强类型枚举试图实现的目标,但有一个小差别:普通枚举可以转换为整数类型,而强类型枚举不能在不使用强制转换的情况下进行转换。

那么,有没有办法将强类型枚举值转换为整数类型而不需要强制转换?如果有,怎么做呢?

14个回答

209

正如其他人所说,你无法进行隐式转换,这是有意为之的。

如果你愿意,你可以避免在转换中指定底层类型。

template <typename E>
constexpr typename std::underlying_type<E>::type to_underlying(E e) noexcept {
    return static_cast<typename std::underlying_type<E>::type>(e);
}

std::cout << foo(to_underlying(b::B2)) << std::endl;

14
根据cppreference.com的说法,C++23将会新增一个std::to_underlying()函数模板用于实现上述功能。 - adentinger
那可能还能更丑吗?这个模板让我眼花缭乱,静态转换让我火冒三丈。标准委员会需要整顿一下。当然,我可以直接移除类的概念,我们就回到了让C/C++一开始如此受欢迎的神奇之处。 - undefined

173

强类型枚举旨在解决多个问题,而不仅仅是您在问题中提到的作用域问题:

  1. 提供类型安全性,从而消除了通过整数提升进行隐式转换为整数的情况。
  2. 指定底层类型。
  3. 提供强大的作用域。

因此,无法将强类型枚举隐式转换为整数,甚至无法将其底层类型隐式转换为整数 - 这就是这个想法。因此,您必须使用 static_cast 来使转换明确。

如果您唯一的问题是作用域,并且您真的希望将其隐式提升为整数,则最好使用未强制类型化的枚举,其作用域为声明它的结构。


71
这是C++创造者又一次“我们更清楚你想做什么”的怪异例子。传统(旧式)枚举类型有许多好处,例如隐式转换为索引、无缝使用位运算等等。新风格的枚举类型添加了一个非常棒的作用域功能,但是...你不能仅使用该功能(即使明确指定基础类型!)。所以现在你要么被迫使用旧式枚举类型,像将它们放入结构体中这样的技巧,要么为新枚举类型创建最丑陋的解决方法,比如创建自己的std::vector包装器来克服那个CAST问题。没有评论。 - avtomaton
3
@avtomaton:你应该知道,C++11中的常规枚举也有作用域。是的,它们将它们的枚举器倒出到全局作用域中(出于向后兼容性的原因),但是你可以通过它们的枚举名称的作用域来访问它们。"为新的枚举创建最丑陋的解决方法,比如创建自己的std::vector包装器来克服那个CAST问题" 或者...只需键入static_cast。这样你的代码会更加清晰易懂。 - Nicol Bolas
9
即使在C++11之前,您也可以通过作用域访问普通的枚举类型,而即使在C++11中,您也可以不使用作用域直接访问它们。使用static_cast只会使代码看起来难看且不易读。如果我已经明确指定了底层类型,为什么还要这样做?旧式枚举类型的解决方案优美而美观,问题在于它们的作用域。新的枚举类型增加了作用域,但却减少了二进制掩码和直接值比较的能力。对我来说这很奇怪。大量此类问题的存在清楚地表明这不仅仅是我的问题。 - avtomaton
4
“在C++11之前,您可以通过作用域访问常规枚举类型。” @avtomaton说。“不,您不能这样做。”(https://gcc.godbolt.org/z/47bP4K) - Nicol Bolas
21
如果你在使用枚举类中的值时仍然需要将MyEnum::Value1强制转换为int,那么指定值类型的能力还有什么意义呢?为什么还要具有指定值类型的功能,如果仍然必须明确将其显式转换为该类型?我不理解。 - Shavais
显示剩余7条评论

128

一个C++14版本的答案,由R. Martinho Fernandes提供:

#include <type_traits>

template <typename E>
constexpr auto to_underlying(E e) noexcept
{
    return static_cast<std::underlying_type_t<E>>(e);
}
与前一个答案一样,这将适用于任何类型的枚举和底层类型。我已添加了 noexcept 关键字,因为它永远不会抛出异常。

更新
这也出现在 Effective Modern C++ by Scott Meyers 中。请参见第10条(在我拥有的书的最后几页中详细说明)。


C++23 版本可以使用 std::to_underlying 函数:

#include <utility>

std::cout << std::to_underlying(b::B2) << std::endl;

...或者如果底层类型可以是1字节类型

std::cout << +(std::to_underlying(b::B2)) << std::endl;

2
8字节 -> 8位? - LorenDB

48

其他回答已经解释了没有隐式转换的原因(出于设计考虑)。

我个人使用一元运算符+将枚举类转换为其基础类型:

template <typename T>
constexpr auto operator+(T e) noexcept
    -> std::enable_if_t<std::is_enum<T>::value, std::underlying_type_t<T>>
{
    return static_cast<std::underlying_type_t<T>>(e);
}

这样可以大大减少"打字开销":

std::cout << foo(+b::B2) << std::endl;

我实际上使用宏一次性创建枚举和运算符函数。
#define UNSIGNED_ENUM_CLASS(name, ...) enum class name : unsigned { __VA_ARGS__ };\
inline constexpr unsigned operator+ (name const val) { return static_cast<unsigned>(val); }

这会让你的代码完全无法阅读。 - undefined
1
嘿 @Stefan,也许你不知道一元+运算符:“对整数操作数进行整数提升” - 所以实际上这是一个很好的解决方案,如果你了解整数提升的话,它非常易读。 - undefined
1
@Dino Dini,好的,明白了。感谢您对一元运算符的澄清。 - undefined

30

简短回答是你无法这样做,正如之前的帖子所指出的那样。但对于我的情况,我只是不想混淆命名空间,但仍然需要隐式转换,所以我只是这样做了:

#include <iostream>

using namespace std;

namespace Foo {
   enum Foo { bar, baz };
}

int main() {
   cout << Foo::bar << endl; // 0
   cout << Foo::baz << endl; // 1
   return 0;
}

通过命名空间对枚举类型进行排序,在不需要将任何枚举值静态转换为底层类型的情况下,增加了一层类型安全性。


8
它完全没有增加任何类型安全性(事实上,您刚刚删除了类型安全性)——它只是增加了名称作用域。 - Lightness Races in Orbit
@LightnessRacesinOrbit 是的,我同意。我谎了。从技术上讲,确切地说,类型只是在名称空间/作用域下面,并完全符合Foo::Foo。成员可以作为Foo::barFoo::baz进行访问,并且可以隐式转换(因此没有太多类型安全性)。如果要开始一个新项目,最好几乎总是使用枚举类。 - solstice333
1
如果代码确实需要将枚举视为整数处理,我认为这种方法是可行的。但是,使用 static_cast<> 可能会降低类型安全性,因为它非常强大,可能会将某些不应该转换的内容进行转换。 - jrh
帮助了我的情况,我特别使用枚举值作为数组索引,在其中两个枚举具有非常相似/相同的名称。 - Lasse Meyer
1
有趣。这解决了我的问题:我只需要一个命名空间符号名称,但它必须与int兼容。现在我必须弄清楚在什么情况下你真的想让枚举也成为自己的类。 - Victor Eijkhout
显示剩余2条评论

26

不,没有自然的方式。

事实上,C++11中拥有强类型enum class的一个动机是防止它们被隐式转换为int


1
看看 Khurshid Normuradov 的回复。它是“自然的方式”,并且与《C++程序设计语言(第四版)》中的意图非常相似。它不是“自动的方式”,这正是它的优点所在。 - PapaAtHome
1
@PapaAtHome,我不明白这与static_cast相比的好处。类型或代码清晰度没有太大变化。这里有什么自然的方式?一个返回值的函数? - Atul Kumar
1
@user2876962 对我来说,好处在于它不是自动或“静默”的,正如Iammilind所说。这可以防止难以发现的错误。你仍然可以进行转换,但你被迫思考。这样你就知道你在做什么。 对我来说,这是安全编码习惯的一部分。如果有一丝可能会引入错误,我更喜欢不进行自动转换。如果你问我,C++11中与类型系统相关的许多变化都属于这个范畴。 - PapaAtHome

21
#include <cstdlib>
#include <cstdio>
#include <cstdint>

#include <type_traits>

namespace utils
{

namespace details
{

template< typename E >
using enable_enum_t = typename std::enable_if< std::is_enum<E>::value, 
                                               typename std::underlying_type<E>::type 
                                             >::type;

}   // namespace details


template< typename E >
constexpr inline details::enable_enum_t<E> underlying_value( E e )noexcept
{
    return static_cast< typename std::underlying_type<E>::type >( e );
}   


template< typename E , typename T>
constexpr inline typename std::enable_if< std::is_enum<E>::value &&
                                          std::is_integral<T>::value, E
                                         >::type 
 to_enum( T value ) noexcept 
 {
     return static_cast<E>( value );
 }

} // namespace utils




int main()
{
    enum class E{ a = 1, b = 3, c = 5 };

    constexpr auto a = utils::underlying_value(E::a);
    constexpr E    b = utils::to_enum<E>(5);
    constexpr auto bv = utils::underlying_value(b);

    printf("a = %d, b = %d", a,bv);
    return 0;
}

4
这并不会减少代码输入量或使代码更加清晰,反而会产生副作用,使得在大型项目中更难找到这些隐式转换。相比于这些构造,使用 static_cast 更容易在整个项目中搜索到。 - Atul Kumar
3
搜索 static_cast 比搜索 to_enum 更容易吗? - Johann Gerell
6
这个答案需要一些解释和文档资料。 - Lightness Races in Orbit
我同意拥有“enum_cast”或类似关键字比到处依赖“static_cast”更好。在读回代码时,我必须查看每个“static_cast”并根据上下文决定它可能正在做什么,而“enum_cast/narrow_cast/up_cast/down_cast”都可以使“static_cast”的操作更清晰,并且通过使用SFINAE语句,我可以确定名称是正在发生的事情。 - Gem Taylor

12

希望这能对你或其他人有所帮助

enum class EnumClass : int //set size for enum
{
    Zero, One, Two, Three, Four
};

union Union //This will allow us to convert
{
    EnumClass ec;
    int i;
};

int main()
{
using namespace std;

//convert from strongly typed enum to int

Union un2;
un2.ec = EnumClass::Three;

cout << "un2.i = " << un2.i << endl;

//convert from int to strongly typed enum
Union un;
un.i = 0; 

if(un.ec == EnumClass::Zero) cout << "True" << endl;

return 0;
}

40
这被称为“类型转换游戏”(type punning),虽然一些编译器支持它,但并不具备可移植性。因为C++标准规定,在你设置了 un.i 之后,它就是“激活成员”,你只能读取联合体的激活成员。 - Jonathan Wakely
8
您说得没错,从技术上讲,但我从未见过在这种情况下编译器不能可靠地工作。像这样的匿名联合、#pragma once等都是事实上的标准。 - BigSandwich
13
当一个简单的类型转换就可以实现时,为什么要使用标准明确禁止的东西呢?这是不正确的。 - Paul Groke
12
有些人认为这种混乱和不确定的行为比简单的 static_cast 更易读,这让我感到绝望。请注意,翻译时不要改变原意。 - underscore_d
2
除非我不使用优化,否则在我的机器上会失败。我想不使用优化是为了在语言中做一些禁止的事情所付出的小代价。 - Eljay
显示剩余3条评论

8
这似乎在原生的enum class中是不可能的,但是你可以使用class来模拟enum class

在这种情况下,

enum class b
{
    B1,
    B2
};

将相当于:

class b {
 private:
  int underlying;
 public:
  static constexpr int B1 = 0;
  static constexpr int B2 = 1;
  b(int v) : underlying(v) {}
  operator int() {
      return underlying;
  }
};

这基本上相当于原始的enum class。你可以在返回类型为b的函数中直接返回b::B1。你可以使用switch case,等等。

根据此示例的精神,您可以使用模板(可能与其他内容一起)来泛化和模拟由enum class语法定义的任何可能的对象。


但是B1和B2必须在类外定义...否则这个案例将无法使用
  • header.h <-- B类
  • main.cpp <---- myvector.push_back( B1 )
- Fl0
这不应该是"static constexpr b"而不是"static constexpr int"吗?否则,b::B1只是一个没有任何类型安全性的int。 - Some Guy

7
C++委员会在将枚举类型从全局命名空间中分离出来方面迈出了一步(scoped enums),但在将枚举类型降解为整数值方面退了50步。遗憾的是,如果您需要以非符号方式使用枚举值,则enum class无法使用。
最佳解决方案是根本不使用它,而是使用命名空间或结构体自己定义的作用域。这两者在此目的上是可互换的。当您提到枚举类型本身时,需要多输入一些内容,但这很可能不会经常发生。
struct TextureUploadFormat {
    enum Type : uint32 {
        r,
        rg,
        rgb,
        rgba,
        __count
    };
};

// must use ::Type, which is the extra typing with this method; beats all the static_cast<>()
uint32 getFormatStride(TextureUploadFormat::Type format){
    const uint32 formatStride[TextureUploadFormat::__count] = {
        1,
        2,
        3,
        4
    };
    return formatStride[format]; // decays without complaint
}

1
很好的解释。枚举类相对于命名空间或结构体中的枚举,有什么单一的优势吗? - Sujay Phadke
有没有使用struct而不是namespace来封装枚举的理由? - jrh
2
哦...我突然想到了一个使用结构体而不是命名空间的原因,它可以防止您意外地定义相同的枚举“命名空间”两次。例如,如果TextureUploadFormat是一个命名空间,您可以多次使用该命名空间并意外合并两个枚举的命名空间。 - jrh
@jrh,主要原因是你无法使用包装命名空间技术来为枚举类型取一个最佳名称:一方面,你希望命名空间本身就是枚举类型的名称(这样你可以写DescriptiveEnumTypeName::Value),但另一方面,你如何给_实际的_ enum 类型命名呢?无论你怎么称呼它,实际类型名称都会出现在声明中,比如DescriptiveEnumTypeName::RealEnumTypeName,这...令人沮丧。 - Sz.
@jrh,主要原因是你无法使用包装命名空间技术来为枚举类型命名得最优。一方面,你希望命名空间本身就是枚举类型的名称(这样你就可以写DescriptiveEnumTypeName::Value),但另一方面,你要如何命名_实际的_ enum 类型呢?无论你如何命名,实际类型名称都会出现在声明中,比如DescriptiveEnumTypeName::RealEnumTypeName,这实在是令人沮丧。 - undefined
然后,如果你最终放弃希望,并向一个struct投降,一个小小的回报是,现在你可以自然地和逐渐地扩展/加固它(虽然有些乏味,也令人沮丧,但至少可行),以实现枚举类的某些好处,如果你以后愿意的话。 - Sz.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接