Шаблон для безопасных флагов класса enum С++ 11

Я пытаюсь создать безопасные флаги С++ с использованием шаблонов. Я также хочу различать флаг a и флаг s (будучи нулем, одним или несколькими флагами).

Приведенное ниже решение работает хорошо, за исключением EnumFlag<T> operator | (T, T), из-за которого все |-операции над перечислениями возвращают тип EnumFlag. Это ломает много кода. Любые трюки, чтобы исправить это? В моем коде я делаю следующее, однако жесткое кодирование Option здесь не вариант. Как сделать это универсальным?

EnumFlag<typename std::enable_if<std::is_same<T, Option>::value, T>::type> operator | (T l, T r)

Изменение этого на...

EnumFlag<T> operator | (T l, T r)

...причины ломает все. Я хотел бы что-то вроде этого (не компилируемый код). Или любая другая лучшая идея!

EnumFlag<typename std::enable_if<std::already_expanded<EnumFlag<T>>::value, T>::type> operator | (T l, T r)

Полный компилируемый код:

EnumFlag.h

#ifndef __classwith_flags_h_
#define __classwith_flags_h_

#include <type_traits>

enum class Option
{
    PrintHi = 1 << 0,
    PrintYo = 1 << 1,
    PrintAlot = 1 << 2
};

template <typename T>
class EnumFlag
{
public:
    using UnderlayingType = typename std::underlying_type<T>::type;

    EnumFlag(const T& flags)
        : m_flags(static_cast<UnderlayingType>(flags))
    {}

    bool operator & (T r) const
    {
        return 0 != (m_flags & static_cast<UnderlayingType>(r));
    }

    static const T NoFlag = static_cast<T>(0);

private:
    UnderlayingType  m_flags;
};
template<typename T>
EnumFlag<typename std::enable_if<std::is_same<T, Option>::value, T>::type> operator | (T l, T r)
{
    return static_cast<T>(static_cast<typename EnumFlag<T>::UnderlayingType>(l) | static_cast<typename EnumFlag<T>::UnderlayingType>(r));
}

class ClassWithFlags
{
public:
    using Options = EnumFlag < Option >;

    void doIt(const Options &options);
};

#endif

EnumFlag.cpp

#include "EnumFlag.h"

#include <iostream>

void ClassWithFlags::doIt(const Options &options)
{
    if (options & Option::PrintHi)
    {
        std::cout << "Hi" << std::endl;
    }
    if (options & Option::PrintYo)
    {
        std::cout << "Yo!" << std::endl;
    }
}

int main()
{
    ClassWithFlags classWithFlags;
    classWithFlags.doIt(Option::PrintHi | Option::PrintAlot);
}

> ДЕМО

Фактический код будет содержать намного больше операторов, однако этого достаточно, чтобы проиллюстрировать проблему.

Это менее навязчивое решение (но все же слишком навязчивое)

template<typename T>
typename std::underlying_type<T>::type operator | (T l, T r)
{
    return (static_cast<typename std::underlying_type<T>::type>(l) | static_cast<typename std::underlying_type<T>::type>(r));
}

Недостаточно бога, тогда EnumFlag(const std::underlying_type<T> &flags) должно существовать, и я теряю безопасность типа. Кроме того, я хотел бы, чтобы глобальные перегрузки операторов создавались только для действительно необходимых типов. Макросы тоже не Бог, потому что я хочу разрешить объявление EnumFlags внутри классов. Глобальных перегрузок быть не может, поэтому мне нужны два вызова макросов в разных местах для создания на EnumFlag.

Решение должно быть чистым C++11/stl.


person Mathias    schedule 06.03.2015    source источник
comment
Разве добавление неявного преобразования из EnumFlag<T> в T не устранит проблемы, вызванные EnumFlag<T> operator | (T l, T r)?   -  person Pradhan    schedule 06.03.2015
comment
Я думаю, что это обходной путь, создание EnumFlag экземпляров, когда вам это не нужно, просто для обратного преобразования. Также это позволит вызывать foo(const Option &o) с экземпляром Options. Следовательно, теряя тип safty. Внутри foo у нас может быть переключатель, обрабатывающий все случаи, но все равно ничего не произойдет...   -  person Mathias    schedule 06.03.2015
comment
Как насчет того, чтобы std::bitset подержать ваши флаги?   -  person Neil Kirk    schedule 06.03.2015


Ответы (3)


У Энтони Уильямса есть хорошая статья с готовым кодом: "Using Enum Классы как битовые поля".

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

Простой пример, демонстрирующий его решение:

#include "bitmask_operators.hpp"

enum class A{
    x=1,y=2
       };

enum class B:unsigned long {
    x=0x80000000,y=0x40000000
        };

template<>
struct enable_bitmask_operators<A>{
    static const bool enable=true;
};

template<>
struct enable_bitmask_operators<B>{
    static const bool enable=true;
};

enum class C{x,y};


int main(){
    A a1=A::x | A::y;
    A a2=a1&A::y;
    a2^=A::x;
    A a3=~a1;

    B b1=B::x | B::y;
    B b2=b1&B::y;
    b2^=B::x;
    B b3=~b1;

    // C c1=C::x | C::y;
    // C c2=c1&C::y;
    // c2^=C::x;
    // C c3=~c1;
}

исходный код bitmask_operators.hpp.

person DJm00n    schedule 07.03.2015
comment
Это хорошая ссылка, но я не полностью удовлетворен необходимостью писать новую спецификацию шаблона для каждого флага. Но это натолкнуло меня на некоторые идеи (но они не увенчались успехом). Примерно так: ... class EnumFlag : is_enum_flag‹T› … EnumFlag‹typename std::enable_if‹std::is_enum_flag‹T›::value, T›::type› оператор | (Тл, Тр)... - person Mathias; 10.03.2015

Вот как я бы это сделал, надеюсь, вы найдете это полезным.

Основная проблема заключается в том, как отличить флаги enum от всех остальных типов. Я бы использовал дополнительный тип шаблона, например. flag_type, в котором будет элемент value только для наших флагов перечисления:

template<typename T>
typename flag_type<T>::value operator | (T, T)
{
    /* implementation */
}

По умолчанию flag_type пусто

template<typename T>
struct flag_type {};

тогда как для флагов перечисления он будет содержать тип EnumFlag, который возвращается из operator|. Вот макрос, который это делает:

#define DECLARE_FLAG_TYPE(__type)                             \
template<>                                                    \
struct flag_type<__type> { using value = EnumFlag<__type>; }

Затем мы можем использовать его для определения флагов перечисления как во внешней области, так и в классах:

enum class Option
{
    One   = 1 << 0,
    Two   = 1 << 1,
    Three = 1 << 2
};
DECLARE_FLAG_TYPE(Option);

class Class
{
public:
    enum class Option
    {
        Four = 1 << 0,
        Five = 1 << 1
    };
};
DECLARE_FLAG_TYPE(Class::Option);
person Kane    schedule 06.03.2015
comment
Спасибо, это хорошее улучшение по сравнению с макросом, который я написал. Оно работает! :D Но все же мне не нравится объявление флага вне класса. Я думаю, что нужно чисто шаблонное решение. - person Mathias; 10.03.2015

Это решение вообще не требует каких-либо дополнительных макросов\использования для объявления класса перечисления:

enum class MyEnum { Value1 = 1 << 0, Value2 = 1 << 1 };
using MyEnums = flags<MyEnum>; // actually this line is not necessary

auto mask = Value1 | Value2; // set flags Value1 and Value 2
if (mask & Value2) { // if Value2 flag is set
    doSomething();
}

https://github.com/grisumbras/enum-flags

person DJm00n    schedule 11.03.2015