【问题标题】:Preventing casting ints to enums in C++防止将整数转换为 C++ 中的枚举
【发布时间】:2018-08-04 18:43:25
【问题描述】:

假设我们有

enum class Month {jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec};

每个值都是一个整数,从 0 到 11。然后我希望 Month 类型的变量只保存这些枚举值。所以这是创建变量的唯一可行方法:

Month m = Month::may;

但这里有一些语言允许的其他方式:

Month m1 = Month(12345);
Month m2 = static_cast<Month>(12345);

这有点令人失望。我如何只允许第一种方式?或者人们如何应对 C++ 中糟糕的枚举?

【问题讨论】:

  • 显然,写第三种方式需要一些努力,如果你使用演员表,你就是在说“我知道我在做什么”。排除第二种方式会很好。
  • C++ 给了你足够的绳索,可以让你射中自己的脚。
  • @Eljay 混合隐喻,我完全同意。

标签: c++ enums


【解决方案1】:

如何只允许第一种方式?

枚举不可能。

如果您想要一个无法从(可能无效的)值显式转换的白痴证明“枚举”,那么您可以使用完整的类而不是枚举。不幸的是,这涉及到一些样板:

struct Month {
    constexpr int value() noexcept { return v; }
    static constexpr Month jan() noexcept { return 0; };
    static constexpr Month feb() noexcept { return 1; };
    static constexpr Month mar() noexcept { return 2; };
    static constexpr Month apr() noexcept { return 3; };
    static constexpr Month may() noexcept { return 4; };
    static constexpr Month jun() noexcept { return 5; };
    static constexpr Month jul() noexcept { return 6; };
    static constexpr Month aug() noexcept { return 7; };
    static constexpr Month sep() noexcept { return 8; };
    static constexpr Month oct() noexcept { return 9; };
    static constexpr Month nov() noexcept { return 10; };
    static constexpr Month dec() noexcept { return 11; };
private:
    int v;
    constexpr Month(int v) noexcept: v(v) {}
};

【讨论】:

    【解决方案2】:

    然后我希望 Month 类型的变量只保存这些枚举值

    那么您误解了枚举(甚至是作用域枚举)是什么。它们为基础类型的 一些 值提供了方便的名称(并且,对于作用域枚举,禁止从该类型进行 隐式 转换)。它们不会将对象限制为那些命名值,也不是有意这样做的。如果您想这样做,请创建一个包含验证例程的类,以更改其状态。

    但是,通常认为这种方法的开销是不值得的。遵循通常的 C++ 实践,只是假设您永远不会给枚举指定不应该的值。如果你这样做了,为什么?这是一个错误:修复它。范围枚举提供的对隐式转换的禁止应该使这些错误几乎不可能消失。如果有人不遗余力地显式转换未命名的值?那是他们自己的错!记录程序的行为将是“未定义的”(不是根据语言,而是根据您自己的代码的 API)并继续。

    【讨论】:

    • “范围枚举提供的对隐式转换的禁止应该使这些错误几乎不可能消失。如果有人不遗余力地显式转换未命名的值?那是他们自己的错! " - 这不是很实用或有用。问题的示例 - 月份枚举数和整数之间的转换 - 是 非常 常见且真正容易出错的要求,例如用于与期望数字(有时基于 0,有时基于 1-)的现有库进行交互,用于计算、序列化和反序列化。有一些枚举需要担心,而其他的则不需要......
    • @TonyDelroy:你可能不认为它“有帮助”,但确实如此。如果有其他方法可以满足该特定用例,它会不会更不容易出错?绝对地!但我们并不生活在那个宇宙中。
    【解决方案3】:

    您的问题可以通过使用封装常规枚举的常规类来解决,如下例所示:

    class Month  {
    public:
      enum Type {
        jan, feb, mar, apr, may, jun,
        jul, aug, sep, oct, nov, dec
      };
      Month(Type t);
    private :
      Type type;
    };
    

    那么以下将产生编译时错误:

      Month mm = Month::jan;
      Month m1 = Month(12345);
      Month m2 = static_cast<Month>(12345);
    
      e.cpp:27:25: error: invalid conversion from 'int' to 'Month::Type' [-
      fpermissive]
          Month m1 = Month(12345);
      e.cpp:26:42: error: invalid conversion from 'int' to 'Month::Type' [-
      fpermissive]
          Month m2 = static_cast<Month>(12345);
    

    仍有一种可能但更复杂的情况需要解决

     Month m1 = Month(Month::Type(12345));
    

    然而,这可以像这样动态检查

     Month::Month(Type t) : type(t){
      if (int(t) < 0 || int(t) > int(jan)) {
          throw "error";
      }
    }
    

    【讨论】:

    • 不错。比我的建议简单得多。虽然,足够不称职的程序员仍然可以写Month(Month::Type(12345))。
    • @user2079303 当然,谢谢我为此添加了一个可能的运行时检查器。
    【解决方案4】:

    您不能在不修改语言本身的情况下禁止该语言允许的事情。毕竟,这是完全可以做到的:

    Month m;
    int * val = (int *) &m;
    *val = -46;
    

    ...没有什么能阻止你。关键是人们通常根本不应该这样做,如果他们这样做,他们通常有一个很好的理由。

    如果您想强制执行更强大的编码策略,您需要一个支持这种诊断的编译器(通常通过警告标志,通常可以转换为硬错误),或者只是一种不同的语言。如果这是不可接受的,人们通常会通过简单地不编写我发布的示例中的疯狂代码来解决此类问题。

    【讨论】:

    • 您的代码导致未定义的行为(严格的别名违规,将枚举作为其底层类型也不例外)
    • 这就是这个例子的重点。没有什么能阻止你编写导致未定义行为的代码。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-08-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多