How do I define / specialize a type_trait in class scope?
Asked Answered
G

2

7

I have the following situation: My problem revolves around using strongly typed enum classes as flags (just as in C# with the Flags-Attribute). I know this is not the way enum classes were meant to be used in the first place, but that it not the point of this question.

I have defined several operators and functions to use on these enum classes, and a custom type trait to distinguish normal enums from Flag-enums. Here's an example:

// Default type_trait which disables the following operators
template <typename T> struct is_flags : std::false_type {};

// Example operator to use enum class as flags
template <typename T>
std::enable_if_t<std::is_enum<T>::value && is_flags<T>::value, T&>
operator|=(T &t1, const T t2)
{
    return t1 = static_cast<T>(static_cast<std::underlying_type_t<T>>(t1) | 
                               static_cast<std::underlying_type_t<T>>(t2));
};

Now if I define any enum class i can do the following:

enum class Foo { A = 1, B = 2 };

enum class Bar { A = 1, B = 2 };

// Declare "Bar" to be useable like Flags
template <> struct is_flags<Bar> : std::true_type {}; 

void test()
{
    Foo f;
    Bar b;
    f |= Foo::A; // Doesn't compile, no operator |=
    b |= Bar::A; // Compiles, type_trait enables the operator
}

The above code works fine and using a macro for the template specialization it almost looks like the very convenient C# Flags-Attribute.

However, when the enum class is not defined in namespace scope, I run into an issue:

struct X
{
    enum class Bar { A = 1, B = 2 };

    // Following line gives: C3412: Cannot specialize template in current scope
    template <> struct is_flags<Bar> : std::true_type {};
}

The type trait cannot be specialized here. I would need to define the trait outside of X, which is possible, but separates the "Flag-Attribute" from the enum declaration. It would be so nice to use this in our code since flags are used all over the place but in a rather old-fashioned manner (int + #define). All solutions to this problem I have found so far focus on classes instead of enums, where the solution is much simpler, since I can define the trait as a member of the class itself. Enums, however, cannot inherit, contain typedefs or whatever might be needed to differentiate a certain enum class from another.

So is there any possibility to define some kind of trait in a class-scope which can be used in global namespace scope to recognize special enum class-types?

EDIT: I should add that I'm using Visual Studio 2013.

UPDATE: Thanks for the answers, the tag-solution worked really well, although I had to make a subtle change (making it even more simple in the process). I'm now using this custom type trait:

template <typename T>
struct is_flags
{
private:
    template <typename U> static std::true_type check(decltype(U::Flags)*);
    template <typename> static std::false_type check(...);

    typedef decltype(check<T>(0)) result;
public:
    static const bool value = std::is_enum<T>::value && result::value;
};

Now, all I need to do is add Flags to the enum class, no matter what scope it's in:

enum class Foo { Flags, A = 0x0001, B = 0x0002 };

See also here for a similar problem and solution.

UPDATE 2: Since Visual Studio 2013 Update 2 this solution will cause compiler crashes when the is_flags trait is applied to ios-base headers. Therefore we are now using a different and cleaner approach, we use a template class which acts as the storage for an enum class and defines all operators on itself without any type-trait magic. The template class can be created implicit with the underlying enum class and explicit with the underlying type. Works a charm and is much less of an enable_if-mess.

Gav answered 10/1, 2014 at 14:6 Comment(7)
Related: How does one use an enum class as a set of flags?Authoritarian
You could use a constexpr function and ADL instead of traits. It's probably ugly, but would allow defining a friend function inside struct X.Authoritarian
@Authoritarian Sounds nice but unfortunately I'm using VS2013, which doesn't support constexpr yet. I added the compiler to the question.Gav
Then you can still use std::true_type and std::false_type (or better yet: a unique type) and a decltype to invoke ADL.Authoritarian
Also related: How to make enum class to work with the 'bit-or' feature?Tooling
It might also be possible to use a C++03-fake enum class (a custom struct) that defines these operators. Something like struct my_enum{ struct enumerator{ constexpr enumerator(int p) : m(p) {} int m; }; constexpr static enumerator A = 1, B = 2; my_enum(enumerator); my_enum& operator |=(my_enum); };Authoritarian
@Authoritarian Yes, but I wanted to stick to the enums because of the way they allow enumerators to be defined. Maybe I focused the question too much on enums, basically I want to find a easy-to-use mechanism for defining an attribute for any type, scoped in a class or not, to be used for type_trait-checking (or some other lookup-mechanism suitable). However, most types provide suitable workarounds, that's why I chose to explain the problem using enums. The issue with enums is that they themselve cannot contain any information other than the values (and underlying type).Gav
V
2

You could tag the enumeration itself:

#include <type_traits>

template<typename T>
struct is_flags {
    private:
    typedef typename std::underlying_type<T>::type integral;
    template<integral> struct Wrap {};

    template<typename U>
    static constexpr std::true_type check(Wrap<integral(U::EnumFlags)>*);

    template<typename>
    static constexpr std::false_type check(...);

    typedef decltype(check<T>(0)) result;

    public:
    static constexpr bool value = std::is_enum<T>::value && result::value;
};

namespace Detail {
    template <bool>
    struct Evaluate;

    template <>
    struct Evaluate<true> {
        template <typename T>
        static T apply(T a, T b) { return T(); }
    };
}

template <typename T>
T evalueate(T a, T b)
{
    return Detail::Evaluate<is_flags<T>::value>::apply(a, b);
}

enum class E{ A = 1, B, C };
struct X {
    enum class F{ EnumFlags, A = 1, B, C };
};

int main ()
{
    // error: incomplete type ‘Detail::Evaluate<false>’ used in nested name specifier
    // evalueate(E::A, E::B);
    evalueate(X::F::A, X::F::B);
}
Vincenzovincible answered 10/1, 2014 at 17:37 Comment(2)
This looks promising, I'll give it a try as soon as possible. My compiler doesn't support constexpr yet, but I guess it won't matter. However when I tried dyps code I ran into some compiler issues with decltype, I hope it works in this case.Gav
Thanks, this works really well, see my updated question with my solution. I had one issue though, the Wrap struct caused problems but that may be due to VS2013 not handling decltype very well (had several compiler crashes while trying other solutions). But in the link I provided in the final solution someone suggested a similar approach without using Wrap at all and this seems to work and is even shorter.Gav
A
2

Here's an ugly solution using ADL instead of traits (of course you can hide the ADL inside the trait):

New operator template:

struct my_unique_enum_flag_type;

// Example operator to use enum class as flags
template <typename T>
enable_if_t<std::is_enum<T>::value
            && std::is_same<decltype(is_flags(std::declval<T>())),
                            my_unique_enum_flag_type>::value, T&>
operator|=(T &t1, const T t2)
{
    return t1 = static_cast<T>(static_cast<underlying_type_t<T>>(t1) | 
                               static_cast<underlying_type_t<T>>(t2));
};

Definition of is_flags for Bar:

struct X
{
    enum class Bar { A = 1, B = 2 };

    friend my_unique_enum_flag_type is_flags(Bar);
};

int main()
{
    X::Bar a = X::Bar::A;
    a |= X::Bar::B;
}

(preferably, use a more unique name than is_flags for ADL)

Authoritarian answered 10/1, 2014 at 15:5 Comment(4)
Hmm that still requires two different macros, since friend must be used inside classes.Authoritarian
I agree, it would be nice to find a way to have 1 macro only to serve as an "attribute" of sorts. But I find this solution already better than my initial approach since the enum and the fact that it can be used as flags is in the same place.Gav
This might be a VS2013-bug but when I try this it doesn't matter if I define the friend or not, it works in any case. It looks like ths std::is_same always returns true although I can't figure out why.Gav
Thanks again for your input, I learned some interesting things about ADL, but Dieter Lückings answer is far more flexible and provides better readability without even using macros.Gav
V
2

You could tag the enumeration itself:

#include <type_traits>

template<typename T>
struct is_flags {
    private:
    typedef typename std::underlying_type<T>::type integral;
    template<integral> struct Wrap {};

    template<typename U>
    static constexpr std::true_type check(Wrap<integral(U::EnumFlags)>*);

    template<typename>
    static constexpr std::false_type check(...);

    typedef decltype(check<T>(0)) result;

    public:
    static constexpr bool value = std::is_enum<T>::value && result::value;
};

namespace Detail {
    template <bool>
    struct Evaluate;

    template <>
    struct Evaluate<true> {
        template <typename T>
        static T apply(T a, T b) { return T(); }
    };
}

template <typename T>
T evalueate(T a, T b)
{
    return Detail::Evaluate<is_flags<T>::value>::apply(a, b);
}

enum class E{ A = 1, B, C };
struct X {
    enum class F{ EnumFlags, A = 1, B, C };
};

int main ()
{
    // error: incomplete type ‘Detail::Evaluate<false>’ used in nested name specifier
    // evalueate(E::A, E::B);
    evalueate(X::F::A, X::F::B);
}
Vincenzovincible answered 10/1, 2014 at 17:37 Comment(2)
This looks promising, I'll give it a try as soon as possible. My compiler doesn't support constexpr yet, but I guess it won't matter. However when I tried dyps code I ran into some compiler issues with decltype, I hope it works in this case.Gav
Thanks, this works really well, see my updated question with my solution. I had one issue though, the Wrap struct caused problems but that may be due to VS2013 not handling decltype very well (had several compiler crashes while trying other solutions). But in the link I provided in the final solution someone suggested a similar approach without using Wrap at all and this seems to work and is even shorter.Gav

© 2022 - 2024 — McMap. All rights reserved.