Реализация операторов для класса enum

После обсуждаемого вопроса Увеличение и уменьшение «enum class», я ' Хочу спросить о возможной реализации арифметических операторов для 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 г.: в одном из документов, посвященных C ++ 17, эта проблема рассматривается, по крайней мере частично, путем разрешения преобразований между переменной класса enum и базовым типом: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0138r2.pdf


person SomeWittyUsername    schedule 16.03.2013    source источник
comment
Приведения обычно указывают на то, что что-то не так ... Приведения нужно использовать. И иногда лучше явное приведение, чем позволять компилятору принимать все, что он хочет.   -  person Yochai Timmer    schedule 16.03.2013
comment
@YochaiTimmer на той же ноте, используя приведение типов, я могу уменьшить все преимущества enum class до исходного enum. Мне это кажется странным - какой смысл вводить новый интегральный тип, основанный на числовом значении, но без встроенной поддержки операций с базовым числовым типом.   -  person SomeWittyUsername    schedule 16.03.2013
comment
Вы можете использовать переключатель. Но это не масштабируется ...   -  person Synxis    schedule 16.03.2013
comment
@Synxis да, это в основном означает создание обходного пути для незавершенности языка в этом аспекте.   -  person SomeWittyUsername    schedule 16.03.2013
comment
Вы можете увидеть итеративное решение, которое будет работать для любых перечислений здесь. Из-за того, как могут быть определены перечисления, потребуются некоторые накладные расходы на память.   -  person Drew Dormann    schedule 16.03.2013
comment
@DrewDormann Это хорошо, но это обман :) На самом деле он использует другую конструкцию (std::set) для преодоления проблем enum. В таком случае почему бы не работать напрямую с std::set вместо enum?   -  person SomeWittyUsername    schedule 16.03.2013
comment
@icepack: Вы тот, кто не хочет делать простое и очевидное приведение. Вы тот, кто считает, что кастинг в определенных ограниченных обстоятельствах - это языковой дефицит. Так что тебе придется жить с последствиями этого.   -  person Nicol Bolas    schedule 16.03.2013
comment
@NicolBolas Я не согласен с вашим утверждением. Во многих случаях выполнение простого гипса значительно облегчит вашу жизнь, по крайней мере, на время. Но в будущем откусит. Я не вижу причин предоставлять целостную конструкцию на языке без целостного набора действительных операций над ней (или, по крайней мере, способа реализовать их естественным образом без принуждения, что, по сути, означает приведение). Да, у enum есть свои проблемы, но enum class решает их, вводя вместо этого другие проблемы   -  person SomeWittyUsername    schedule 16.03.2013
comment
@icepack: Я не вижу причин предоставлять целостную конструкцию на языке без целостного набора допустимых операций над ней Если вам нужны эти конструкции, вы не должны использовать enum class. Вся цель этого состоит в том, чтобы не неявно преобразовывать в целое число. Чтобы не рассматривать его как целочисленный тип. Следовательно, если вы хотите выполнить над ним определенные целочисленные операции, вы должны определить их самостоятельно.   -  person Nicol Bolas    schedule 16.03.2013
comment
@icepack: Но он откусит в будущем. Нет, не будет; это догматическое мышление. Вы делаете кастинг только в определенных, определенных и изолированных местах.   -  person Nicol Bolas    schedule 16.03.2013
comment
@NicolBolas Я вижу 2 проблемы с этим. Во-первых, если он не может быть преобразован в целое число (или double, float и т. Д.), Какова цель указания базового типа? Если только разрешить явное приведение к типу, мы вернемся к исходному enum. В этом случае проще использовать enum. Во-вторых, я не запрашиваю преобразование в целочисленный тип. Я прошу выполнить итерацию (например) по диапазону допустимых значений без обращения или использования каким-либо образом базового целочисленного типа.   -  person SomeWittyUsername    schedule 16.03.2013
comment
@NicolBolas Я не противник кастинга, я согласен с тем, что это приемлемо, но я не вижу достаточных аргументов в этом конкретном случае.   -  person SomeWittyUsername    schedule 16.03.2013
comment
используя приведение типов, я могу уменьшить все преимущества enum class до исходных enum - нет. Преимущество enum class в том, что он не неявно преобразуется в целочисленный тип, а не в том, что он не может быть преобразован в целочисленный тип. Если вы хотите рассматривать данный enum class как интегральный тип, вы можете сделать это и можете сделать это явно внутри своего enum class интерфейса. Да, добавление итераций по элементам и подобной поддержки на языке было бы неплохо, но был добавлен enum class, потому что это было и полезно, и легко.   -  person Yakk - Adam Nevraumont    schedule 16.03.2013
comment
@Yakk, а как бы вы реализовали ++ для несмежных enum class (даже с приведениями)? Может быть, это только я, но определение enum class мне кажется недоработанным. Это типобезопасный, но менее полезный. Он эффективно разрешает сочетание двух разных типов (что очень важно), но также удаляет все неявно поддерживаемые операции стандартных перечислений (кроме сравнения). Да, enum также не поддерживает итерацию по несмежному диапазону, но с enum class удаляется даже итерация по непрерывному диапазону.   -  person SomeWittyUsername    schedule 16.03.2013
comment
@icepack Вы продолжаете говорить, как будто приведение enum class к базовому типу невозможно или нежелательно. Вам лично это не нравится: но это не дизайн enum class. В enum class вы можете получить доступ к базовому типу в любой момент через приведение, и это является частью дизайна. Доступ к операторам базового типа не удаляется, это просто больше не происходит неявно. И несмежные enums? Если вы не приведете enum к базовому типу, значение enum не будет означать ничего.   -  person Yakk - Adam Nevraumont    schedule 16.03.2013
comment
@Yakk Это правда. Но enum class - это абстракция языка более высокого уровня. Фундаментальная часть любой абстракции - скрыть технические детали, не относящиеся к клиенту. Если я хочу представить набор фруктов как {яблоко, банан, лимон}, мне не очень интересно добавлять 1 к базовой реализации apple, чтобы добраться до банана. Я использую его для выполнения операций на том же уровне, что и определение моего типа. Вот почему я использую абстракции - чтобы не разбираться с лежащими в основе техническими деталями. Но с enum class я все равно вынужден с ними иметь дело. Я вижу здесь противоречие.   -  person SomeWittyUsername    schedule 16.03.2013
comment
И пользователю enum class не обязательно. Но кто-то, реализующий operator++ на enum class, является частью реализации и должен возиться с деталями реализации. Обратите внимание, что не каждый enum class ограничен значениями внутри {} - вы можете написать битовое поле, которое переопределяет |&^~, а алгебраическое замыкание - все допустимые элементы enum class. Для такого типа следующая enum операция недопустима. enum class просто позволяет разработчику выбирать, когда рассматривать его как базовый тип, он не блокирует доступ.   -  person Yakk - Adam Nevraumont    schedule 17.03.2013
comment
@Yakk Итак, мы видим функцию enum class в другом свете. Я рассматриваю это как отдельный целочисленный перечислительный тип. Так же, как операции с double не требуют никаких операций с int или любым другим интегральным типом, я ожидаю, что операции с enum class будут вести себя одинаково в этом аспекте.   -  person SomeWittyUsername    schedule 17.03.2013


Ответы (3)


Решение без приведения в действие - использовать переключатель. Однако вы можете сгенерировать псевдопереключатель, используя шаблоны. Принцип заключается в рекурсивной обработке всех значений перечисления с использованием списка шаблонов (или пакета параметров). Итак, вот 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-ish (доступен здесь):

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)

Вы можете расширить его, добавив некоторый препроцессор, чтобы избавиться от необходимости повторять список значений.

person Synxis    schedule 16.03.2013
comment
Действительно, очень красиво! Конечно, необходимость в таких вещах не облегчает жизнь разработчику. Мне кажется естественным, что такая инфраструктура изначально была построена в стандарте C ++ 11. - person SomeWittyUsername; 17.03.2013
comment
Мне нравится версия шаблонов, потому что вы можете использовать их не только для оператора приращения: вы можете добавлять имена и т.д. только для компиляторов C ++ 03). Кроме того, я сделал метод в стиле C ++ 1 менее подробным. - person Synxis; 17.03.2013

Каждый оператор в 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;
  }
}

и аналогично для уменьшения, и реализовать приращение на n как циклы повторяющихся increment.

person Yakk - Adam Nevraumont    schedule 16.03.2013
comment
Это лучшее, что я могу придумать, но, как вы сказали, это смехотворно многословно и, что еще важнее, не масштабируемо. - person SomeWittyUsername; 16.03.2013
comment
@SomeWittyUsername: enum class был создан именно для того, чтобы сделать это невозможным (сделать это случайно; поэтому для этого требуется смехотворное количество прыжков через обруч). Если этот защитный механизм стоит на вашем пути, просто не используйте enum class, только старый enum. - person SF.; 05.05.2016
comment
вы можете использовать следующий код для безопасного преобразования перечислений: melpon.org/wandbox/permlink/8eJuKbrjem8q8srL - person Jan Christoph Uhde; 13.10.2016
comment
@jan ты уверен? Если значение базового типа «вне гаммы», законно ли приведение к перечислению? Возможно, стоит ТАК вопрос. - person Yakk - Adam Nevraumont; 13.10.2016
comment
@JanChristophUhde Нет, меня беспокоит гамма. Я знаю, что перечисления гарантируют, что значения в битовой гамме своих значений являются действительными. Я не уверен, гарантируют ли они, что все значения в базовом типе могут быть преобразованы в значения перечисления определенным образом. (Я понимаю, что наивная реализация перечислений, по-видимому, делает это, конечно, это сработает; я спрашиваю, знаете ли вы, поддерживает ли стандарт это предположение.) - person Yakk - Adam Nevraumont; 13.10.2016
comment
melpon.org/wandbox/permlink/8eJuKbrjem8q8srL - извините, вы были быстрее, чем я думал. обновили пример, если что-то не так, будет предупреждение. и нет никакого способа преобразовать long long в int, значение либо подходит, либо нет. в конце концов это static_cast - person Jan Christoph Uhde; 13.10.2016
comment
@JanChristophUhde Я говорю о приведении базового типа к перечислению. Кажется, вы не знаете ответа на вопрос. Придется читать стандарт. - person Yakk - Adam Nevraumont; 13.10.2016
comment
expr.static.cast [5.2.9 / 10] Значение не изменяется, если исходное значение находится в диапазоне значений перечисления (7.2). В противном случае поведение не определено. 7.2 / 8 [dcl.enum] подразумевает, что с нефиксированным базовым типом не все значения базового типа являются допустимыми значениями перечисления, и преобразование за пределами этого диапазона - UB. - person Yakk - Adam Nevraumont; 13.10.2016
comment
Да, приведение long к int, выходящему за пределы допустимого диапазона, очевидно, неверно. Опять же а) вы можете проверить равенство типов. б) вы, скорее всего, получите предупреждение. - person Jan Christoph Uhde; 13.10.2016
comment
@JanChristophUhde Нет, enum bob{x=1;}; bob x = (bob)std::underlying_type_t<bob>(-1);, насколько я могу судить, является неопределенным поведением в соответствии со стандартом. Это не имеет ничего общего с длинными или целыми числами. Это приведение базового типа перечисления без фиксированного базового типа к перечислению. Я процитировал стандарт, который, кажется, утверждает, что bob не имеет фиксированного базового типа (но у него есть базовый тип, он просто не называется фиксированным по стандарту), и существуют разные правила для преобразования в перечисление для типа с нет фиксированного базового типа. - person Yakk - Adam Nevraumont; 13.10.2016

enum class Colors { Black, Blue, White };

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

Проверить в оболочке C ++

person vigord    schedule 19.07.2017
comment
Это начинает вызывать проблемы, как только некоторые значения перечисления имеют явные значения. См., Например, cpp.sh/6pa7t. - person moggi; 19.07.2017