【问题标题】:Detecting if casting an int to an enum results into a non-enumerated value检测是否将 int 转换为枚举会导致非枚举值
【发布时间】:2011-01-25 16:20:50
【问题描述】:

假设我有这样的事情:

enum CardColor { HEARTS, DIAMONDS, CLUBS, SPADES};

CardColor MyColor = static_cast<CardColor>(100);

是否有一种(简单的)方法可以在编译时或运行时检测MyColor 的值不对应于任何枚举值?

更一般地,如果枚举值没有相互跟随,例如:

enum CardColor { HEARTS = 0, DIAMONDS, CLUBS = 4, SPADES};

【问题讨论】:

  • @CashCow :好吧,两者都有!我更新了我的问题。
  • @James:我知道我不应该那样做,但这并不能阻止我好奇! :-)
  • 没有从枚举的基础类型到枚举类型的隐式转换是有充分理由的。因此,在使用 CardColor 时,请考虑 CardColor 术语,而不是可以显式转换为 CardColor 的整数。

标签: c++ enums


【解决方案1】:

CashCow 向这个问题提出a decent answer:编写自定义函数来执行检查强制转换当然很简单。

不幸的是,这也是很多工作,您必须确保它与枚举保持同步,以便枚举定义中的枚举数列表与检查强制转换函数中的枚举数列表相同。您还必须为您希望能够对其执行检查强制转换的每个枚举编写其中一个。

我们可以使用预处理器(在 Boost 预处理器库的帮助下)自动生成所有这些代码,而不是做所有这些手动工作。这是一个生成枚举定义以及checked_enum_cast 函数的宏。它看起来可能有点吓人(代码生成宏通常看起来很可怕),但熟悉它是一种非常有用的技术。

#include <stdexcept>
#include <boost/preprocessor.hpp>

// Internal helper to provide partial specialization for checked_enum_cast
template <typename Target, typename Source>
struct checked_enum_cast_impl;

// Exception thrown by checked_enum_cast on cast failure
struct invalid_enum_cast : std::out_of_range 
{ 
    invalid_enum_cast(const char* s)
        : std::out_of_range(s) { }
};

// Checked cast function
template <typename Target, typename Source>
Target checked_enum_cast(Source s)
{
    return checked_enum_cast_impl<Target, Source>::do_cast(s);
}

// Internal helper to help declare case labels in the checked cast function
#define X_DEFINE_SAFE_CAST_CASE(r, data, elem) case elem:

// Macro to define an enum with a checked cast function.  name is the name of 
// the enumeration to be defined and enumerators is the preprocessing sequence
// of enumerators to be defined.  See the usage example below.
#define DEFINE_SAFE_CAST_ENUM(name, enumerators)                           \
    enum name                                                              \
    {                                                                      \
        BOOST_PP_SEQ_ENUM(enumerators)                                     \
    };                                                                     \
                                                                           \
    template <typename Source>                                             \
    struct checked_enum_cast_impl<name, Source>                            \
    {                                                                      \
        static name do_cast(Source s)                                      \
        {                                                                  \
            switch (s)                                                     \
            {                                                              \
            BOOST_PP_SEQ_FOR_EACH(X_DEFINE_SAFE_CAST_CASE, 0, enumerators) \
                return static_cast<name>(s);                               \
            default:                                                       \
                throw invalid_enum_cast(BOOST_PP_STRINGIZE(name));         \
            }                                                              \
            return name();                                                 \
        }                                                                  \
    };

以下是您将如何将其与 CardColor 示例一起使用:

DEFINE_SAFE_CAST_ENUM(CardColor, (HEARTS) (CLUBS) (SPADES) (DIAMONDS))

int main()
{
    checked_enum_cast<CardColor>(1);   // ok
    checked_enum_cast<CardColor>(400); // o noez!  an exception!
}

第一行替换您的enum CardColor ... 定义;它定义了枚举并提供了一种特殊化,允许您使用checked_enum_cast 将整数转换为CardColor

这可能看起来很麻烦,只是为您的枚举获取一个检查的强制转换函数,但这种技术非常有用且可扩展。您可以添加执行各种操作的函数。例如,我有一个生成函数来将枚举类型转换为字符串表示和执行其他几种转换和检查的函数,这些函数用于我的大多数枚举。

记住,你必须编写和调试那个又大又丑的宏一次,然后你就可以在任何地方使用它。

【讨论】:

    【解决方案2】:

    最简单的运行时解决方案是不使用 static_cast,而是使用为您进行检查的函数。如果你把你的枚举放在一个类中,你可以通过类来做到这一点。 比如:

    class CardCheck
    {
    public:
      enum CardColor { HEARTS, DIAMONDS, CLUBS, SPADES };
    
      explicit CardCheck( int x ) : c( static_cast< CardColor >( x ) )
      {
         switch( c )
         {
           case HEARTS: case DIAMONDS: case CLUBS: case SPADES:
              break;
    
           default:
             // assert or throw
        }
      }
    
      CardColor get() const
      {
         return c;
      }
    
     private:
      CardColor c;      
    };
    

    【讨论】:

    • 演员应该在检查之后进行:它可能会导致溢出(我认为)。
    • 有趣的是,我们在代码中做了类似的事情来验证整数被转换为枚举。一个编译器决定优化 switch 语句,大概是因为所有可能的情况都被覆盖了,所以它假设它总是有效的。包含 CARD_COUNT 可能是一个很好的理由。那么至少有一个可能的错误值并且编译器不会优化。
    【解决方案3】:

    clang 支持动态溢出检查。请参阅-fsanitize=enum 开关。使用此开关编译的程序将通过 stderr 输出指示枚举分配错误。这将允许您进行调试测试。官方版本不适合测试可疑输入。

    【讨论】:

      【解决方案4】:

      通常在枚举的末尾有一个附加元素,指示其中的项目数。您可以使用该值在运行时检查该值是否有效:

      enum CardColor { HEARTS, DIAMONDS, CLUBS, SPADES, CARDS_COUNT};
      
      CardColor MyColor = static_cast<CardColor>(100);
      
      if (MyColor >= CARDS_COUNT) {
          /* Invalid value */
      }
      

      【讨论】:

      • 不,不是。您真的希望可接受的 CardColor 为 CARDS_COUNT 个吗?
      • @wheaties 实际上,任何整数都是可接受的 CardColor。这就是为什么首先提出这个问题的原因。
      • 我认为一个问题是static_cast&lt;CardColor&gt;(100); 会触发未定义的行为,这意味着理论上返回的值甚至可能是 HEARTS 或 DIAMONDS。见stackoverflow.com/a/33608071/2436175
      • @wheaties 我必须同意 vz0。这是我工作的常见做法,也是处理连续枚举的好方法。您可以使用 CARDS_COUNT 初始化容器,可以使用值作为终止符使用 for 循环对其进行迭代,并且可以使用它进行各种健全性检查。命名约定清楚地表明它是一个特殊值,不应像其他值一样使用。您只需要确保所有枚举值都是连续的。所以这是对 OP 的第一个例子的解决方案,但它并没有推广到他的第二个例子。
      • 编译器根据值选择枚举的大小。只要值适合表示,该值就是有效的,即使它在技术上是未定义的行为。它与例如将 int64_t 分配给 int32_t 没有什么不同。您最好在向下转换时启用编译器警告。
      【解决方案5】:

      一开始 - 这样做是个坏主意。

      但如果你愿意,我建议硬编码枚举的整数值:

      enum CardColor { HEARTS = 10, DIAMONDS = 11, CLUBS = 12, SPADES = 13};
      

      然后重载赋值运算符:

      CardColor operator = (int value)
      {
          switch (value)
          {
              case 10:
                  return HEARTS;
              // case for other values
              default:
                  // throw an exception or something
          }
      }
      

      【讨论】:

      • 虽然这在技术上可以满足 OP 的要求,但它也完全使使用枚举的点无效。 @CashCow 的回答类似,但要好得多。
      【解决方案6】:

      枚举值可能重叠或有孔;此外,可以将实际变量分配为零、集合中的任何值或允许值的按位或。所以:

      enum suites { hearts, diamonds, clubs, spades };
      

      允许值 0、1、2、3;

      enum suites { hearts = 1 << 0, diamonds = 1 << 1, clubs = 1 << 2, spades = 1 << 4 };
      

      允许从 0 到 15 的任何值。

      如果您使用枚举来定义位值,通常最好定义(二进制)operator&amp;operator|operator&amp;=operator|=。如果您不这样做,则每当生成不在集合中的值时,您都需要显式强制转换,因此很容易发现发生这种情况的地方。

      如果分配了允许范围之外的数字,或者如果不是没有,则编译器可以警告第一个或所有名称都附加了初始值设定项(这违反了MISRA-C 规则)。

      【讨论】:

      • 所以小丑要么是 0(无花色)要么是 15(全部 4 个)?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-12-30
      • 1970-01-01
      • 2020-08-21
      • 2021-01-04
      • 1970-01-01
      • 1970-01-01
      • 2010-09-06
      相关资源
      最近更新 更多