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

скажем, у меня есть

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 operator&(Flags lhs, Flags rhs)
{
    return static_cast<Flags>(static_cast<std::uint16_t>(lhs) & static_cast<std::uint16_t>(rhs));
}

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

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

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

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

person Billy ONeal    schedule 19.06.2014    source источник
comment
Я тоже хотел бы знать. В настоящее время я обычно делаю что-то вроде if(f & static_cast<uint16_t>(Flags::A)) всякий раз, когда мне это нужно.   -  person shuttle87    schedule 19.06.2014


Ответы (2)


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

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

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

if (!!(f & Flags::A)) {

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

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

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

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

person David Rodríguez - dribeas    schedule 19.06.2014
comment
Then again, if you really want implicit conversions, why are you using an enum class in the first place? Потому что enum class отключает вредоносные неявные преобразования в/из int. По той же причине у нас есть явные операторы преобразования в C++11 — мы хотим разрешить конкретное использование (проверка флага в if) и предотвратить вредоносное использование (случайная передача перечисления флагов где-то как int или случайная передача int в качестве перечисления флагов). - person Billy ONeal; 19.06.2014
comment
@BillyONeal Если вы хотите избежать вредных неявных преобразований, не добавляйте их для bool. Хотя вы ограничиваете область преобразований, вы намеренно подвергаете ту же проблему, которую пытаетесь избежать, используя строго типизированные перечисления. - person Captain Obvlious; 19.06.2014
comment
@Captain: Неправда. explicit operator bool не допускает неявное преобразование в/из int, что здесь является вредным поведением. Вот почему весь этот оператор явного преобразования и механизм контекстного преобразования были добавлены в язык с самого начала. - person Billy ONeal; 19.06.2014
comment
Я знаю это, но operator bool() здесь даже не применимо, если только вы не впихнете все в класс. - person Captain Obvlious; 19.06.2014
comment
@Captain: Тогда ответ на мой вопрос, могу ли я сделать класс enum контекстно конвертируемым в bool, - нет, вы не можете. Я в порядке с этим ответом. - person Billy ONeal; 19.06.2014
comment
На самом деле я собираюсь использовать не только перечисления строгого типа, но вы можете выдумать что-то очень близкое. Смотрите мой ответ. - person Captain Obvlious; 20.06.2014

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

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

Код:

#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 Flags& 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 operator&(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 operator&(Flags lhs, Flags rhs)
{
    return static_cast<Flags::Enum>(
        static_cast<std::uint16_t>(lhs)
        & static_cast<std::uint16_t>(rhs));
}

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

inline Flags operator&=(Flags& lhs, Flags rhs)
{
    return lhs = lhs & 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 & Flags::FLAG1) {}
    f &= Flags::FLAG1;

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

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

person Captain Obvlious    schedule 20.06.2014
comment
+1. К сожалению, пока это не сработает для моего приложения (пытаюсь обновить устаревший код C, у которого есть проблемы с конструкторами и деструкторами), но я рассмотрю это в будущем. :) - person Billy ONeal; 20.06.2014
comment
Это прискорбно. Надеюсь, это решит вашу проблему или, по крайней мере, приблизит вас к желаемому поведению. - person Captain Obvlious; 20.06.2014
comment
@CaptainObvlious, если этот класс передается по значению аналогично перечислениям (или любым другим фундаментальным типам), интересно, сможет ли компилятор оптимизировать его для передачи, например, в регистр (вместо memcpy и т. д.). По сути, повлечет ли это дополнительные накладные расходы по сравнению с обычным перечислением? (Извините, что избил дохлую лошадь.) - person Innocent Bystander; 14.07.2017