枚举类操作符的实现

42

在讨论“枚举类”的递增和递减的问题之后,我想询问是否有可能为enum class类型实现算术运算符。

以下是来自原始问题的示例:

enum class Colors { Black, Blue, White, END_OF_LIST };

// Special behavior for ++Colors
Colors& operator++( Colors &c ) {
  c = static_cast<Colors>( static_cast<int>(c) + 1 );
  if ( c == Colors::END_OF_LIST )
    c = Colors::Black;
  return c;
}

有没有一种方法可以实现算术运算符而不需要强制转换到已定义运算符的类型?我想不出任何方法,但类型强制转换让我感到困扰。类型强制转换通常表示某些东西出了问题,必须有非常好的理由使用它们。我希望语言允许实现一个运算符而不强制转换到特定类型。

2018年12月更新: C++17 的其中一篇论文似乎至少部分解决了这个问题,允许在枚举类变量和基础类型之间进行转换:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0138r2.pdf


17
"铸件通常是某些问题的指示" ... 铸件既然存在就需要使用。有时候明确地进行类型转换比让编译器自行决定更好。 - Yochai Timmer
8
@icepack:是你不想做简单明显的转换。是你认为在某些有限的情况下需要转换是语言上的缺陷。因此,你将不得不承担这样做的后果。 - Nicol Bolas
9
如果您想要这些构造,就不应该使用枚举类。它的整个目的是不被隐式转换为整数���它不应该被当作整数类型来处理。因此,如果您想对其执行某些整数操作,则必须自己定义它们。@icepack说:“我认为在语言中提供一个积分构造没有一个积分集上有效的操作是没有理由的。” - Nicol Bolas
4
“@icepack:“但将来它会被咬掉。” 不,不会;那是教条主义的思维方式。你只在某些特定且孤立的地方做投射。” - Nicol Bolas
5
通过使用类型转换,我可以将“枚举类”所有的优点降到与原来的“枚举”相同水平。但是,“枚举类”的优势在于它不会隐式地转换为整型类型,而不是不能转换为整型类型。当您想将给定的“枚举类”视为整型类型时,您可以这样做,并且可以在您的“枚举类”接口内明确指定。是的,在语言中添加对元素的迭代等支持是一件好事,但是“枚举类”的添加是因为它既有用又容易实现。 - Yakk - Adam Nevraumont
显示剩余13条评论
3个回答

43

无需转换的解决方案是使用switch语句。但是,您可以使用模板生成伪开关。其原理是使用模板列表(或参数包)递归处理枚举的所有值。因此,这里介绍了我发现的3种方法。

测试枚举:

enum class Fruit
{
    apple,
    banana,
    orange,
    pineapple,
    lemon
};

香草开关(点此查看实例)

Fruit& operator++(Fruit& f)
{
    switch(f)
    {
        case Fruit::apple:     return f = Fruit::banana;
        case Fruit::banana:    return f = Fruit::orange;
        case Fruit::orange:    return f = Fruit::pineapple;
        case Fruit::pineapple: return f = Fruit::lemon;
        case Fruit::lemon:     return f = Fruit::apple;
    }
}

C++03风格的方法(点此查看)

template<typename E, E v>
struct EnumValue
{
    static const E value = v;
};

template<typename h, typename t>
struct StaticList
{
    typedef h head;
    typedef t tail;
};

template<typename list, typename first>
struct CyclicHead
{
    typedef typename list::head item;
};

template<typename first>
struct CyclicHead<void,first>
{
    typedef first item;
};

template<typename E, typename list, typename first = typename list::head>
struct Advance
{
    typedef typename list::head lh;
    typedef typename list::tail lt;
    typedef typename CyclicHead<lt, first>::item next;

    static void advance(E& value)
    {
        if(value == lh::value)
            value = next::value;
        else
            Advance<E, typename list::tail, first>::advance(value);
    }
};

template<typename E, typename f>
struct Advance<E,void,f>
{
    static void advance(E& value)
    {
    }
};

/// Scalable way, C++03-ish
typedef StaticList<EnumValue<Fruit,Fruit::apple>,
        StaticList<EnumValue<Fruit,Fruit::banana>,
        StaticList<EnumValue<Fruit,Fruit::orange>,
        StaticList<EnumValue<Fruit,Fruit::pineapple>,
        StaticList<EnumValue<Fruit,Fruit::lemon>,
        void
> > > > > Fruit_values;

Fruit& operator++(Fruit& f)
{
    Advance<Fruit, Fruit_values>::advance(f);
    return f;
}

C++11风格的方法(点击此处查看)

template<typename E, E first, E head>
void advanceEnum(E& v)
{
    if(v == head)
        v = first;
}

template<typename E, E first, E head, E next, E... tail>
void advanceEnum(E& v)
{
    if(v == head)
        v = next;
    else
        advanceEnum<E,first,next,tail...>(v);
}

template<typename E, E first, E... values>
struct EnumValues
{
    static void advance(E& v)
    {
        advanceEnum<E, first, first, values...>(v);
    }
};

/// Scalable way, C++11-ish
typedef EnumValues<Fruit,
        Fruit::apple,
        Fruit::banana,
        Fruit::orange,
        Fruit::pineapple,
        Fruit::lemon
> Fruit_values11;

Fruit& operator++(Fruit& f)
{
    Fruit_values11::advance(f);
    return f;
}

(C++11老版本)

你可以尝试通过添加一些预处理器来避免重复值列表的需求。


1
非常棒!毫无疑问,这种东西的需要并没有让开发人员的生活变得更加轻松。在我看来,这样的基础设施在C++11标准中被原生地构建是很自然的事情。 - SomeWittyUsername
1
我喜欢模板版本,因为你可以将它们用于不仅仅是增量运算符:你可以添加名称等等... C++03-ish方法适用于不支持可变参数模板的编译器,并且可以适应简单的枚举(仅适用于C++03编译器)。此外,我使C++1-ish方法更加简洁。 - Synxis

6

在 C++ 中,枚举类型的每个操作都可以不用转换为基础类型进行编写,但这样做会使代码变得异常冗长。

举个例子:

size_t index( Colors c ) {
  switch(c) {
    case Colors::Black: return 0;
    case Colors::Blue: return 1;
    case Colors::White: return 2;
  }
}
Color indexd_color( size_t n ) {
  switch(n%3) {
    case 0: return Colors::Black;
    case 1: return Colors::Blue;
    case 2: return Colors::White;
  }
}
Colors increment( Colors c, size_t n = 1 ) {
  return indexed_color( index(c) + n );
}
Colors decrement( Colors c, size_t n = 1 ) {
  return indexed_color( index(c)+3 - (n%3) );
}
Colors& operator++( Colors& c ) {
  c = increment(c)
  return c;
}
Colors operator++( Colors& c, bool ) {
  Colors retval = c;
  c = increment(c)
  return retval;
}

一个智能编译器将能够将这些转换为直接作用于基本整数类型的操作。

但是,在您的enum class接口中转换为基本整数类型并不是一件坏事。而且,运算符是您的enum class的接口的一部分。

如果您不喜欢通过size_t循环并认为它是一个虚假的转换,那么您可以写:

Colors increment( Colors c ) {
  switch(c) {
    case Colors::Black: return Colors::Blue;
    case Colors::Blue: return Colors::White;
    case Colors::White: return Colors::Black;
  }
}

对于减法也是同样的道理,通过反复执行increment实现nincrement来实现increment-by-n


这也是我能想到的最好的方法,但正如你所说,“它非常冗长”,更重要的是-不可扩展。 - SomeWittyUsername
1
@SomeWittyUsername:enum class 的创建正是为了防止意外操作(因此需要大量的跳跃环节才能有意地执行)。如果这种保护机制妨碍了你的工作,请不要使用 enum class,而是使用旧的 enum - SF.
你可以使用以下代码以安全的方式转换枚举:http://melpon.org/wandbox/permlink/8eJuKbrjem8q8srL - Jan Christoph Uhde
@JanChristophUhde 不,我担心的是色域。我知道枚举保证其值在“位色域”内是有效的。但我不确定它们是否保证底层类型中的所有值都能以定义的方式转换为枚举值。(我知道枚举的天真实现似乎使这个假设成立;我想知道你是否知道标准是否支持这种假设。) - Yakk - Adam Nevraumont
@JanChristophUhde • 这个链接已经失效了。 - Eljay
显示剩余7条评论

1
enum class Colors { Black, Blue, White };

Colors operator++(Colors& color)
{
    color = (color == Colors::White) ? Colors::Black : Colors(int(color) + 1);
    return color;
}

在C++ Shell中检查


2
一旦某些枚举值具有显式值,这就会开始引起问题。例如,请参见 http://cpp.sh/6pa7t - moggi

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