如何检查枚举值是否有效?

63

我正在从二进制文件中读取一个enum值,并想检查该值是否真的是enum值的一部分。 我该怎么做?

#include <iostream>

enum Abc
{
    A = 4,
    B = 8,
    C = 12
};

int main()
{
    int v1 = 4;
    Abc v2 = static_cast< Abc >( v1 );

    switch ( v2 )
    {
        case A:
            std::cout<<"A"<<std::endl;
            break;
        case B:
            std::cout<<"B"<<std::endl;
            break;
        case C:
            std::cout<<"C"<<std::endl;
            break;
        default :
            std::cout<<"no match found"<<std::endl;
    }
}

我必须使用switch操作符吗?还有更好的方法吗?

编辑

我已经设置了枚举值,不幸的是我不能修改它们。 更糟的是,它们不连续(它们的值为0, 75,76,80,85,90,95,100等)。


3
任何枚举都只是一个数字,所以我认为没有比这更好的检查方法。你可能应该为你的数据类型定义一个更严谨的结构。 - Rizo
11个回答

26

enum枚举类型在C++中是有效的,如果它在范围[A, B]之内,由以下标准规则定义。因此,在enum X { A = 1, B = 3 }的情况下,2被认为是一个有效的枚举值。

请参考标准文件7.2/6:

对于最小的枚举项emin和最大的枚举项emax,枚举类型的值是基础类型中的值,其范围为bmin到bmax,其中bmin和bmax分别是可以存储emin和emax的最小位域的最小值和最大值。可以定义具有未由任何枚举器定义的值的枚举。

C++中没有反射机制。一种方法是额外列出枚举值并编写一个包装器,该包装器将执行转换并在失败时可能引发异常。

有关如何将int强制转换为枚举的更多详细信息,请参见类似问题


3
你误解了标准引用,有效值不仅限于[A,B] - Matthieu M.
16
确实,例如如果这些值是1和5,那么后者至少需要3位二进制表示,因此6和7也将是枚举类型的有效值。 - visitor
9
这个答案仍然是错误的。 @visitor 指出了一个反例,证明了你提供的例子是错误的。他说“如果值为1和5,则后者至少需要3位,因此6和7也将是枚举器的有效值”。你的答案意味着在这种情况下只有{1、2、3、4、5}是有效的值。你对标准的引用是正确的,但你的例子会引人误解。 - mannyglover

17
在C++ 11中,如果你愿意将枚举值列为模板参数,就有一种更好的方法。你可以把这看作是一件好事,它允许你在不同的上下文中接受有效枚举值的子集;当从外部源代码解析时通常很有用。
下面示例的一个可能有用的补充是在EnumType的底层类型与IntType之间加入一些静态断言,以避免截断问题。这留给你作为练习。
#include <stdio.h>

template<typename EnumType, EnumType... Values>
class EnumCheck;

template<typename EnumType>
class EnumCheck<EnumType>
{
public:
    template<typename IntType>
    static bool constexpr is_value(IntType) { return false; }
};

template<typename EnumType, EnumType V, EnumType... Next>
class EnumCheck<EnumType, V, Next...> : private EnumCheck<EnumType, Next...>
{
    using super = EnumCheck<EnumType, Next...>;

public:
    template<typename IntType>
    static bool constexpr is_value(IntType v)
    {
        return v == static_cast<IntType>(V) || super::is_value(v);
    }
};

enum class Test {
    A = 1,
    C = 3,
    E = 5
};

using TestCheck = EnumCheck<Test, Test::A, Test::C, Test::E>;

void check_value(int v)
{
    if (TestCheck::is_value(v))
        printf("%d is OK\n", v);
    else
        printf("%d is not OK\n", v);
}

int main()
{
    for (int i = 0; i < 10; ++i)
        check_value(i);
}

由于在编译时无法确定'int v'的值,因此is_value必须在运行时执行。这难道不会导致各种递归函数调用,并且与简单的switch语句或所有值的数组相比非常低效吗?您仍然需要列出所有枚举值,因此使用此方法并没有任何好处。或者我错过了什么? - Super-intelligent Shade
1
@InnocentBystander 这些都是constexpr函数,因此编译器有很多优化的空间。这些函数也不是递归的;它们是一系列同名函数的链。在对上面的示例进行一些快速测试时,gcc 5.4生成的模板版本代码比开关版本少一个指令。Clang 3.8的模板版本则多两个指令。结果将取决于值的数量以及值是否连续。特别是在协议解码时,最大的优势是你可以在一行上编写你期望的代码。 - janm
3
没错,抱歉我说错了,应该是函数调用的链而不是递归。有趣的是编译器可以优化掉这些。感谢您对三年前回答的跟进 :) - Super-intelligent Shade

16

也许可以像这样使用枚举类型:

enum MyEnum
{
A,
B,
C
};

并且要检查

if (v2 >= A && v2 <= C)
如果不为枚举常量指定值,则值从零开始,每向下移动一个常量增加一。例如,给定 enum MyEnumType { ALPHA, BETA, GAMMA }; ALPHA 的值为 0,BETA 的值为 1,GAMMA 的值为 2。

2
我喜欢这种简单的方法,并通过始终将枚举中的第一项定义为SOMETYPE_UNKNOWN,最后一项定义为SOMETYPE_MAX来扩展它。然后测试将始终是AssertTrue(v2> = SOMETYPE_UNKNOWN && v2 <= SOMETYPE_MAX)。当然,在UNKNOWN和MAX之间只能添加项目。 - Elise van Looij

7

Managed Extensions for C++支持以下语法:

enum Abc
{
    A = 4,
    B = 8,
    C = 12
};

Enum::IsDefined(Abc::typeid, 8);

参考资料:MSDN "扩展 C++ 程序设计"


我不确定什么是“managed c++”,但你确定它是c++而不是c#吗?这个链接看起来像是c#。 - BЈовић
4
“Managed C++”是微软的一种C++语言变体,可以使用“ .NET框架”的库。由于在C#中未定义“::”运算符,因此它看起来像是C++。请注意,这里的翻译并未改变原文意思,同时以通俗易懂的方式表达了它。 - Stefan
@BЈовић,你在托管扩展 C++ 项目中尝试过这段代码吗?我们在一个 C++ 项目中使用类似的代码。具体来说,我们使用 Enum::IsDefined() 方法。 - Brett
1
@Brett 抱歉,我不知道“托管扩展 C++ 项目”是什么。 - BЈовић
@BЈовић 我在答案中添加了对MSDN“C++托管扩展编程”的引用。希望这有所帮助。 - Brett

7
我发现让这个过程“简单”的唯一方法是创建(宏)一个已排序的枚举数组并检查它。
使用switch技巧处理enum时会失败,因为enum可能具有给定值的多个枚举器。
这真是一个令人烦恼的问题。

5

虽然有些过时,但是……将整数的范围检查到第一个/最后一个枚举值(可以与Janm的想法相结合,以进行精确的检查),使用C++11:

头文件:

namespace chkenum
{
    template <class T, T begin, T end>
    struct RangeCheck
    {
    private:
        typedef typename std::underlying_type<T>::type val_t;
    public:
        static
        typename std::enable_if<std::is_enum<T>::value, bool>::type
        inrange(val_t value)
        {
            return value >= static_cast<val_t>(begin) && value <= static_cast<val_t>(end);
        }
    };

    template<class T>
    struct EnumCheck;
}

#define DECLARE_ENUM_CHECK(T,B,E) namespace chkenum {template<> struct EnumCheck<T> : public RangeCheck<T, B, E> {};}

template<class T>
inline
typename std::enable_if<std::is_enum<T>::value, bool>::type
testEnumRange(int val)
{
    return chkenum::EnumCheck<T>::inrange(val);
}

枚举声明:

enum MinMaxType
{
     Max = 0x800, Min, Equal
};
DECLARE_ENUM_CHECK(MinMaxType, MinMaxType::Max, MinMaxType::Equal);

使用方法:

bool r = testEnumRange<MinMaxType>(i);

以上提出的主要区别在于测试函数仅依赖于枚举类型本身。


2
谈到语言,枚举值只存在于编译时,无法以程序方式枚举。但是,通过精心设计的基础设施,您仍然可以避免多次列出所有值。请参见Easy way to use variables of enum types as string in C?中提供的“enumFactory.h”,您的示例可以进行重写。
#include "enumFactory.h"

#define ABC_ENUM(XX) \
    XX(A,=4) \
    XX(B,=8) \
    XX(C,=12) \

DECLARE_ENUM(Abc,ABC_ENUM)

int main()
{
    int v1 = 4;
    Abc v2 = static_cast< Abc >( v1 );

    #define CHECK_ENUM_CASE(name,assign) case name: std::cout<< #name <<std::endl; break;
    switch ( v2 )
    {
        ABC_ENUM(CHECK_ENUM_CASE)
        default :
            std::cout<<"no match found"<<std::endl;
    }
    #undef CHECK_ENUM_CASE
}

甚至可以使用头文件中已经存在的一些更多设施:
#include "enumFactory.h"

#define ABC_ENUM(XX) \
    XX(A,=4) \
    XX(B,=8) \
    XX(C,=12) \

DECLARE_ENUM(Abc,ABC_ENUM)
DEFINE_ENUM(Abc,ABC_ENUM)

int main()
{
    int v1 = 4;
    Abc v2 = static_cast< Abc >( v1 );
    const char *name = GetString(v2);
    if (name[0]==0) name = "no match found";
    std::cout << name << std::endl;
}

2

一种不需要查找的解决方案 - 枚举值应该是质数

这种解决方案并非普适:

  • 不适用于标志枚举
  • 不适用于巨大的枚举或者枚举值(会溢出)
  • 可能难以保持一致性

但是实用:

  • O(1)复杂度

  • 能够添加INVALID=1作为错误指示的枚举定义

代码:

#include<iostream>
#include <cstdint>
#include <vector>
enum class Some :uint64_t{
   A=2,
   B=3,
   C=5,
   D=7,
   E=11,
   F=13,
// etc. just stick to primes
};
static constexpr uint64_t some_checksum = static_cast<uint64_t>(Some::A)*
static_cast<uint64_t>(Some::B)*
static_cast<uint64_t>(Some::C)*
static_cast<uint64_t>(Some::D)*
static_cast<uint64_t>(Some::E)*
static_cast<uint64_t>(Some::F);

constexpr bool is_some(uint64_t v) {
    return some_checksum % v == 0;
}
constexpr bool get_some(uint64_t v, Some& out){
    if (some_checksum % v == 0) {
        out = static_cast<Some>(v);
        return true;
    }
    return false;//Something to indicate an error;
}

int main(int v) {
    Some s;
    if (get_some(v, s)){
        std::cout << "Ok\n" << static_cast<int>(s) << "\n"; 
    } else {
        std::cout << "No\n";
    }

}

0

另一种方法是:

#include <algorithm>
#include <iterator>
#include <iostream>

template<typename>
struct enum_traits { static constexpr void* values = nullptr; };

namespace detail
{

template<typename T>
constexpr bool is_value_of(int, void*) { return false; }

template<typename T, typename U>
constexpr bool is_value_of(int v, U)
{
    using std::begin; using std::end;

    return std::find_if(begin(enum_traits<T>::values), end(enum_traits<T>::values),
        [=](auto value){ return value == static_cast<T>(v); }
    ) != end(enum_traits<T>::values);
}

}

template<typename T>
constexpr bool is_value_of(int v)
{ return detail::is_value_of<T>(v, decltype(enum_traits<T>::values) { }); }

////////////////////
enum Abc { A = 4, B = 8, C = 12 };

template<>
struct enum_traits<Abc> { static constexpr auto values = { A, B, C }; };
decltype(enum_traits<Abc>::values) enum_traits<Abc>::values;

enum class Def { D = 1, E = 3, F = 5 };

int main()
{
    std::cout << "Abc:";
    for(int i = 0; i < 10; ++i)
        if(is_value_of<Abc>(i)) std::cout << " " << i;
    std::cout << std::endl;

    std::cout << "Def:";
    for(int i = 0; i < 10; ++i)
        if(is_value_of<Def>(i)) std::cout << " " << i;
    std::cout << std::endl;

    return 0;
}

我个人认为这种方法的“丑陋”之处在于必须定义:

decltype(enum_traits<Abc>::values) enum_traits<Abc>::values

如果您不反对使用宏,您可以将其包装在一个宏内部:

#define REGISTER_ENUM_VALUES(name, ...) \
template<> struct enum_traits<name> { static constexpr auto values = { __VA_ARGS__ }; }; \
decltype(enum_traits<name>::values) enum_traits<name>::values;

遗憾的是,所有类似方法的丑陋部分都是枚举的重复。 - Burak

0

对于那些使用 C++17 的人来说,另一个选择是使用 折叠表达式

template<typename T, typename ...Args>
constexpr typename std::enable_if_t<std::conjunction_v<std::is_same<T, Args>...>, std::optional<T>>
ToOneOf( typename std::underlying_type_t<T> value, Args&& ...args ) noexcept
{
    static_assert( std::is_enum_v<T>, "'T' must be of type enum." );

    using U = typename std::underlying_type_t<T>;
    std::array<T, sizeof...( Args )> values{ std::forward<Args>( args )... };

    const auto it{ std::find_if( std::cbegin( values ), std::cend( values ), 
        [ value ]( auto e ) { return static_cast<U>( e ) == value; } ) };

    return it != std::end( values ) ? std::optional<T>{ *it } : std::nullopt;
}   

template<typename T, typename ...Args>
constexpr typename std::enable_if_t<std::conjunction_v<std::is_same<T, Args>...>, bool>
IsOneOf( typename std::underlying_type_t<T> value, Args&& ...args ) noexcept
{
    static_assert( std::is_enum_v<T>, "'T' must be of type enum." );

    using U = typename std::underlying_type_t<T>;
    return ( ... || ( value == static_cast<U>( args ) ) );
}

enum class Test
{
    E0 = 12,
    E1 = 56,
    E2 = 101
};

int main( )
{
    if ( IsOneOf<Test>( 12, Test::E0, Test::E1, Test::E2 ) )
    {
        std::cout << 12 << " is a valid enum value\n";
    }

    if ( auto opt{ ToOneOf<Test>( 56, Test::E0, Test::E1, Test::E2 ) } )
    {
         std::cout << static_cast<int>( opt.value( ) )  << " is a valid enum value\n";
    }
}

仍需要复制枚举。你想要的是能够扩展枚举,而其他所有内容都可以在不修改的情况下继续工作。就这方面而言,这个例子实际上比简单的“switch”更糟糕,因为使用“switch”时,当你没有处理所有枚举值情况时,至少可以生成警告。 - Jaap Versteegh
IsOneOf 只是检查 12 是否是这里 3 个命名值之一。enum 还可以保存没有名称的值。特别地,0 总是合法的。找到那些未命名但合法的值是一个难题。 - MSalters
@MSalters 我相信这就是所需的。OP想要确保读取的值是enum中明确指定的变量之一。 - WBuck

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