Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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

like image 853
Violet Giraffe Avatar asked Dec 21 '25 19:12

Violet Giraffe


1 Answers

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.

like image 152
StoryTeller - Unslander Monica Avatar answered Dec 24 '25 10:12

StoryTeller - Unslander Monica



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!