Home > Software engineering >  What is wrong with my application of SFINAE when trying to implement a type trait?
What is wrong with my application of SFINAE when trying to implement a type trait?

Time:05-11

I needed a type trait that decays enums to their underlying type, and works the same as decay_t for all other types. I've written the following code, and apparently this is not how SFINAE works. But it is how I thought it should work, so what exactly is the problem with this code and what's the gap in my understanding of C ?

namespace detail {
    template <typename T, std::enable_if_t<!std::is_enum_v<T>>* = nullptr>
    struct BaseType {
        using Type = std::decay_t<T>;
    };

    template <typename T, std::enable_if_t<std::is_enum_v<T>>* = nullptr>
    struct BaseType {
        using Type = std::underlying_type_t<T>;
    };
}

template <class T>
using base_type_t = typename detail::BaseType<T>::Type;

The error in MSVC is completely unintelligible:

'detail::BaseType': template parameter '__formal' is incompatible with the declaration

In GCC it's a bit better - says that declarations of the second template parameter are incompatible between the two BaseType templates. But according to my understanding of SFINAE, only one should be visible at all for any given T and the other one should be malformed thanks to enable_if.

Godbolt link

CodePudding user response:

SFINAE applied to class templates is primarily about choosing between partial specialisations of the template. The problem in your snippet is that there are no partial specialisations in sight. You define two primary class templates, with the same template name. That's a redefinition error.

To make it work, we should restructure the relationship between the trait implementations in such as way that they specialise the same template.

namespace detail {
    template <typename T, typename = void> // Non specialised case
    struct BaseType {
        using Type = std::decay_t<T>;
    };

    template <typename T>
    struct BaseType<T, std::enable_if_t<std::is_enum_v<T>>> {
        using Type = std::underlying_type_t<T>;
    };
}

template <class T>
using base_type_t = typename detail::BaseType<T>::Type;

The specialisation provides a void type for the second argument (just like the primary would be instantiated with). But because it does so in a "special way", partial ordering considers it more specialised. When substitution fails (we don't pass an enum), the primary becomes the fallback.

You can provide as many such specialisation as you want, so long as the second template argument is always void, and all specialisations have mutually exclusive conditions.

CodePudding user response:

BaseType isn't being partial-specialized, you're just redeclaring it, and since the non-type parameter has a different type, the compilation fails. you might want to do

#include <type_traits>

namespace detail {
  template <typename T, bool = std::is_enum_v<T>>
    struct BaseType;

    template <typename T>
    struct BaseType<T, false> {
      using Type = std::decay_t<T>;
    };

    template <typename T>
    struct BaseType<T, true> {
      using Type = std::underlying_type_t<T>;
    };
}

CodePudding user response:

You declare the same struct with different parameter, which is forbidden.

You can do it with partial specialization:

namespace detail {
    template <typename T, typename Enabler = void>
    struct BaseType {
        using Type = std::decay_t<T>;
    };

    template <typename E>
    struct BaseType<E, std::enable_if_t<std::is_enum_v<E>>>
    {
        using Type = std::underlying_type_t<E>;
    };
}

Demo

  • Related