Можно ли сделать перечисление с ограниченной областью действия («enum class») контекстуально конвертируемым в bool?

#c #enums #enum-class

#c #перечисления #enum-class

Вопрос:

Допустим, у меня есть

 enum class Flags : std::uint16_t
{
    None = 0,
    A    = 0x0001,
    B    = 0x0002,
    C    = 0x0004
}

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

inline Flags operatoramp;(Flags lhs, Flags rhs)
{
    return static_cast<Flags>(static_cast<std::uint16_t>(lhs) amp; static_cast<std::uint16_t>(rhs));
}

inline Flags operator|=(Flagsamp; lhs, Flags rhs)
{
    return lhs = lhs | rhs;
}

inline Flags operatoramp;=(Flagsamp; lhs, Flags rhs)
{
    return lhs = lhs amp; rhs;
}
 

Возможно ли сделать класс enum контекстуально конвертируемым в bool, чтобы позволить кому-то делать

 Flags f = /* ... */;
if (f amp; Flags::A) {
    // Do A things
}
 

Комментарии:

1. Я также хотел бы знать. В настоящее время я обычно обнаруживаю, что делаю что-то вроде if(f amp; static_cast<uint16_t>(Flags::A)) всякий раз, когда мне это нужно.

Ответ №1:

Я не думаю, что вы можете предоставить оператор преобразования bool , поскольку реального экземпляра класса не существует, но вы можете перегружать другие операторы. Естественным было бы operator! :

 bool operator!(Flags f) {
   return f == Flags::None;
}
 

Тогда ваша программа будет работать:

 if (!!(f amp; Flags::A)) {
 

Что на самом деле неестественно, но это не было бы ужасно удивительно для других (что касается того, что это значит, они, вероятно, были бы озадачены двойным отрицанием).

Альтернативно, вы можете реализовать операцию как именованную функцию, чтобы сделать ее более читаемой:

 bool test(Flag f, Flag mask) {
   return !!(f amp; mask);
}
if (test(f,Flags::A)) { …
 

Опять же, если вы действительно хотите неявных преобразований, зачем вы вообще используете класс enum?

Комментарии:

1. Then again, if you really want implicit conversions, why are you using an enum class in the first place? Потому enum class что отключает вредные неявные преобразования в / из int . Это та же причина, по которой у нас есть явные операторы преобразования в C 11 — мы хотим разрешить конкретное использование (тестирование флага в an if ) и предотвратить вредное использование (случайная передача перечисления flags где-либо в качестве an int или случайная передача an int в качестве перечисления flags).

2. @BillyONeal Если вы хотите избежать вредных неявных преобразований, не добавляйте один для bool . Хотя вы ограничиваете объем преобразований, вы намеренно подвергаете той же проблеме, которую пытаетесь избежать, используя строго типизированные перечисления.

3. @Captain: Неверно. explicit operator bool не допускает неявного преобразования в / из int , что является вредным поведением здесь. Вот почему весь этот явный оператор преобразования и механизм контекстного преобразования были добавлены в язык для начала.

4. Я знаю это, но operator bool() здесь это даже не применимо, если вы не помещаете все в класс.

5. На самом деле я имею в виду «не только с перечислениями строгих типов, но вы можете придумать что-то очень близкое». Смотрите мой ответ.

Ответ №2:

Хотя вы не можете выполнить это только с помощью строго типизированных перечислений, вы можете инкапсулировать тип перечисления и преобразования в классе, чтобы получить поведение, подобное тому, что вы ищете. Для его объединения требуется немного больше усилий, но не так много, это будет громоздко (если вы не используете десятки базовых флагов enum. В этом случае может быть желательно решение на основе шаблона.

Инкапсулируя его в класс, вы получаете все необходимые операторы преобразования, необходимые для выполнения операций, описанных в вашем вопросе. Эти преобразования идут в обоих направлениях и в сочетании с операторами в области пространства имен обеспечивают (я надеюсь) поведение, которого вы пытаетесь достичь.

Код:

 #include <cstdint>

class Flags
{
    enum class Enum : std::uint16_t
    {
        EMPTY = 0, FLAG1 = 1, FLAG2 = 2, FLAG3 = 4, FLAG4 = 8
    };

public:

    //  Default constructor. At least you'll have default initialization.
    Flags() : value_(EMPTY) {}

    //  Basic copy-ctor
    Flags(const Flagsamp; value) : value_(value.value_) {}

    //  Conversion-ctor allowing implicit conversions. This allows the
    //  non-member operators to work.
    Flags(Enum value) : value_(value) {}

    //  We want to be able to expose and use the strongly typed enum.
    operator Enum() const
    {
        return value_;
    }

    //  In order to simplify the manipulation of the enum values we
    //  provide an explicit conversion to the underlying type.
    explicit operator std::uint16_t() const
    {
        return static_cast<std::uint16_t>(value_);
    }

    //  Here's your magical bool conversion.
    explicit operator bool() const
    {
        return value_ != EMPTY;
    }

    //  Let's make some friends so Enum can continue to be a hermit.
    friend inline Flags operator|(Flags::Enum lhs, Flags::Enum rhs);
    friend inline Flags operatoramp;(Flags lhs, Flags rhs);

    //  As a convenience we declare the enumeration values here. This allows
    //  scoping similar to the typed enums.
    static const Enum EMPTY = Enum::EMPTY;
    static const Enum FLAG1 = Enum::FLAG1;
    static const Enum FLAG2 = Enum::FLAG2;
    static const Enum FLAG3 = Enum::FLAG3;
    static const Enum FLAG4 = Enum::FLAG4;

private:

    Enum  value_;
};



inline Flags operator|(Flags::Enum lhs, Flags::Enum rhs)
{
    return static_cast<Flags::Enum>(
        static_cast<std::uint16_t>(lhs)
        | static_cast<std::uint16_t>(rhs));
}

inline Flags operatoramp;(Flags lhs, Flags rhs)
{
    return static_cast<Flags::Enum>(
        static_cast<std::uint16_t>(lhs)
        amp; static_cast<std::uint16_t>(rhs));
}

inline Flags operator|=(Flagsamp; lhs, Flags rhs)
{
    return lhs = lhs | rhs;
}

inline Flags operatoramp;=(Flagsamp; lhs, Flags rhs)
{
    return lhs = lhs amp; rhs;
}

void Func(Flags)
{
    // do something really cool here
}

int main()
{
    Flags    f;

    // equality
    if (f) {}
    if (!f) {}

    // operations and more equality
    f |= Flags::FLAG1;
    if (f amp; Flags::FLAG1) {}
    f amp;= Flags::FLAG1;

    // Call a function after doing some ops on the plain enum values
    Func(Flags::FLAG1 | Flags::FLAG2);
}
 

Одним из недостатков, который я вижу в этом, является то, что он плохо сочетается с признаками типа, связанными с перечислением (т.Е. std::underlying_type ).

Комментарии:

1. 1. К сожалению, пока это не сработает для моего приложения (пытаюсь обновить устаревший код C, у которого есть проблемы с конструкторами и деструкторами), но я рассмотрю это в будущем. 🙂

2. Это прискорбно. Надеюсь, это решит вашу проблему или, по крайней мере, приблизит вас к желаемому поведению.

3. @CaptainObvlious если этот класс передается по значению способом, аналогичным перечислениям (или любым другим фундаментальным типам), мне интересно, сможет ли компилятор оптимизировать его для передачи, например, в регистр (вместо memcpy и т. Д.). В принципе, будет ли это сопряжено с дополнительными накладными расходами по сравнению с обычное перечисление? (Извините за избиение мертвой лошади.)