【问题标题】:How to use C++11 enum class for flags如何使用 C++11 枚举类作为标志
【发布时间】:2015-09-15 05:53:38
【问题描述】:

假设我有这样一堂课:

enum class Flags : char
{
    FLAG_1 = 1;
    FLAG_2 = 2;
    FLAG_3 = 4;
    FLAG_4 = 8;
};

现在我可以拥有一个具有类型标志的变量并分配一个值7 吗?我可以这样做吗:

Flags f = Flags::FLAG_1 | Flags::FLAG_2 | Flags::FLAG_3;

Flags f = 7;

出现这个问题是因为在枚举中我没有为7 定义值。

【问题讨论】:

  • 我认为这是可能的。但是您的代码无法编译!
  • 如果您之后尝试测试Flags 变量,则未定义的行为(在char 之间来回转换之后)。我可以尝试在标准中为您找到它,但您应该自己做。
  • @CoffeeandCode 定义完美。
  • 真的吗?它不适合我。
  • @T.C.可以肯定的是,尝试使用枚举类未定义的值来测试枚举类变量的值会受到未定义行为的影响(尽管不确定)。但是,如果您针对基础类型对其进行测试,我知道它肯定已定义。

标签: c++ c++11 enums


【解决方案1】:

您需要编写自己的重载operator|(可能还有operator& 等)。

Flags operator|(Flags lhs, Flags rhs) 
{
    return static_cast<Flags>(static_cast<char>(lhs) | static_cast<char>(rhs));
}

只要值在枚举值范围内(否则为 UB;[expr.static.cast]/p10),将整数转换为枚举类型(范围或不范围)是明确定义的。对于具有固定基础类型的枚举(这包括所有作用域枚举;[dcl.enum]/p5),枚举值的范围与基础类型的值范围相同([dcl.enum]/p8)。如果底层类型不固定,规则会更棘手 - 所以不要这样做:)

【讨论】:

  • within the range of enumeration values 意味着
  • @Narek 枚举值的范围与底层类型的值范围相同,即std::numeric_limits&lt;char&gt;::min()std::numeric_limits&lt;char&gt;::max()。通常,[-128, 127] 或 [0, 255],取决于您的 char 的签名。
  • 您可以随时转换为std::underlying_type&lt;Flags&gt;::type,然后您无需指定基础类型,它仍将始终在范围内。
  • @sjdowling 如果底层类型不固定,那么它不会影响枚举值的范围,并且强制转换为它是不够的。 (我相信那里的规则使|&amp; 仍然安全,但~ 可能会破坏。)RL 示例:GCC bug 56158
【解决方案2】:

使用std::underlying_type 可能比硬编码char 类型更好。

Flags operator|(Flags lhs, Flags rhs) {
    return static_cast<Flags>(
        static_cast<std::underlying_type<Flags>::type>(lhs) |
        static_cast<std::underlying_type<Flags>::type>(rhs)
    );
}

现在,您可以更改枚举的基础类型,而无需在每个位运算符重载中更新该类型。

【讨论】:

    【解决方案3】:

    请不要这样做。如果你需要这样做,enum classs 可能不是你需要的。

    @T.C.只要您指定底层类型,就向您展示了如何做到这一点,但是您会遇到程序执行不应该执行的操作的地方。

    例如,您使用switch 并为每个定义 枚举值设置case

    例如

    enum class my_enum: unsigned int{
        first = 1,
        second = 2,
        third = 4,
        fourth = 8
    };
    
    int main(){
        auto e = static_cast<my_enum>(static_cast<unsigned int>(my_enum::first) | static_cast<unsigned int>(my_enum::second));
    
        switch(e){
            case my_enum::first:
            case my_enum::second:
            case my_enum::third:
            case my_enum::fourth:
                return 0;
        }
    
        std::cout << "Oh, no! You reached a part of the program you weren't meant to!\n";
        return 1;
    }
    

    将输出:

    Oh, no! You reached a part of the program you weren't meant to!
    

    然后返回错误码1

    当然,这也是为什么您应该始终拥有 default 案例的一个示例,但这不是我的意思。

    当然,您可以争辩说,只要enum class 的用户除了传递给函数之外,从不直接使用该值;这将是限制位集值的好方法。但我总是有点太值得信赖了,我发现 std::uint[n]_t 和一些 constexpr 变量是最好的方法(如果用户设置了无效位,它根本什么都不做)。

    您所做的并不真正适合enum class,因为它违背了具有范围枚举的目的。如果您将其设置为未定义的值,您将无法再枚举这些值。

    【讨论】:

    • Qt 我猜这有很多标志来启用或禁用一个功能,对吧?
    • 我想他们使用 enum 并将其视为 int,这对于限制位集的值很有用。
    • 老实说,我认为这与enum 无关。如果您只有 onetwothreefour 的直接变量,您将得到相同的结果。在我看来,这个例子的问题在于名称的选择会导致意想不到的结果,而不是 enums 固有的任何东西。
    • @Galik 名称不应该是数字 1、2、3、4 的表示;而是第一个可能的值,第二个可能的值等。我会改变它以更好地表示
    • 错误在于将 位掩码类型 ([bitmask.types]) 视为枚举类型(在标准库的意义上,[enumerated .types]),而不是选择实现位掩码类型的方式。用户代码应该将位掩码类型视为不透明的,而作用域枚举通过防止隐式转换来帮助实现这一点。
    【解决方案4】:

    有问题的代码无法编译。但是你可以做这样的事情,

    enum class Flags : char
    {
        FLAG_1 = 1,
        FLAG_2 = 2,
        FLAG_3 = 4,
        FLAG_4 = 8,
    };
    
    int main() {
        Flags f = static_cast<Flags>(7);
        Flags f1 = static_cast<Flags>( static_cast<char>(Flags::FLAG_1) | static_cast<char>(Flags::FLAG_2) | static_cast<char>(Flags::FLAG_3) );
        return 0;
    }
    

    works

    【讨论】:

      【解决方案5】:

      它应该处理任何枚举类型。我不确定它没有任何副作用并且是完全有效的 C++ 代码。如果有问题请告诉我。

      template<class T, std::enable_if_t<std::is_enum_v<T>, int> = 0>
      constexpr T operator|(T lhs, T rhs) 
      {
          return static_cast<T>(
              static_cast<std::underlying_type<T>::type>(lhs) | 
              static_cast<std::underlying_type<T>::type>(rhs));
      }
      

      【讨论】:

        【解决方案6】:

        我意识到这个问题有点老了,但我会写一个我用来做这个的方法。

        (如果有的话,如果我以后再用谷歌搜索一下,我会记录下来再次查找。)

        我个人喜欢这种方法,因为智能感知(至少是它的 VSCode 版本...我在 Linux 上没有 Visual Studio...)会自动了解您正在做的事情并为您提供有用的提示。此外,它避免使用宏,因此编译器可以在不满意时警告您。最后,没有 cmets,它的代码量并不多。您没有创建一个类并设置一堆重载或任何东西,但您仍然可以获得范围枚举的好处,以便您可以为另一个枚举重用标志名称/值。无论如何都要上例子。

        namespace FlagsNS
        {
            /* This is an old/classic style enum so put it in a
            namespace so that the names don't clash
            (ie: you can define another enum with the values of 
            Flag_1 or Flag_2, etc... without it blowing up)
            */
            enum Flags
            {
                Flag_1 = 1 << 0, //Same as 1
                Flag_2 = 1 << 1, //Same as 2
                Flag_3 = 1 << 2, //Same as 4
                Flag_4 = 1 << 3 //Same as 8
            };
        }
        /* This is telling the compiler you want a new "type" called Flags
        but it is actually FlagsNS::Flags. This is sort of like using the
        #define macro, except it doesn't use the preprocessor so the
        compiler can give you warnings and errors.
        */
        using Flags = FlagsNS::Flags;
        
        //Later in code.... so int main() for example
        int main()
        {
            //If you don't mind c-style casting
            Flags flag = (Flags)(Flags::FLAG_1 | Flags::FLAG_2 | Flags::FLAG_3);
            //Or if you want to use c++ style casting
            Flags flag = static_cast<Flags>(Flags::FLAG_1 | Flags::FLAG_2 | Flags::FLAG_3);
        
            //Check to see if flag has the FLAG_1 flag set.
            if (flag & Flags::FLAG_1)
            {
                //This code works
            }
        }
        

        【讨论】:

        • 这是一个有趣的问题,使用命名空间在以前的枚举类中很流行。我还没有尝试过,但我认为它不会帮助意外使用第二个枚举定义中的枚举值?我的意思是,我们的类型安全性与枚举类不同,对吧?
        • 只要您不在同一个命名空间中创建另一个枚举,第二个枚举定义应该没有任何问题。唯一命名空间允许枚举与枚举类非常相似地工作,底部的using 语句使您无需记住唯一命名空间,因此它就像支持标志的枚举类一样。
        【解决方案7】:

        此时,定义自己的类来处理这个问题可能是有意义的。

         /** Warning: Untested code **/
         struct Flag {
        
             static Flag Flag_1;
             static Flag Flag_2;
             static Flag Flag_3;
             static Flag Flag_4;
        
             Flag operator = (Flag);
         private:
             char const value;
         };
        
         Flag operator | (Flag, Flag); 
        

        【讨论】:

        • 仅仅存储一些位就太过分了。 std::bitset 比创建自己的类更有意义。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多