Search code examples
c++c++-templates

Best way to use SFINAE to disable an otherwise-non-templated member function of a class template?


There are many similar-looking questions, but I can't find quite this one: Given

template <typename T>
struct S {
    int f(int x) const { return x; }
};

I want to be able to SFINAE-away f depending on some metafunction of T. The obvious thing doesn't work:

#include <type_traits>

template <typename T>
struct MyProperty { static constexpr auto value = std::is_same_v<T, float>; };

template <typename T>
struct S {
    template <typename = std::enable_if_t<MyProperty<T>::value>>
    int f(int x) const { return x; }
};

int main() {
    S<int> s;
}

The closest I can come is a dummy typename U = T:

    template <typename U = T, typename = std::enable_if_t<MyProperty<U>::value>>
    int f(int x) const { 
        static_assert(std::is_same_v<T, U>, "Don't provide U.");
        return x;
    }

which works https://godbolt.org/z/8qGs6P8Wd but feels round-about. Is there a better way?


Solution

  • If you can use C++20 or later, add a constraint:

    template<class T>
    struct S {
        int f(int x) const requires MyProperty<T>::value { 
            return x;
        }
    };
    

    In C++17, you could do it like you do it now or move the enable_if to be used for the function's return type. If you put the condition std::is_same_v<U, T> in the enable_if too instead of in a static_assert, it'll be more SFINAE friendly in case you want to enable other fs.

    template<class T>
    struct S {
        template<class U = T>
        std::enable_if_t<std::is_same_v<U, T> && MyProperty<U>::value, int> 
        f(int x) const { 
            return x;
        }
    };
    

    Another option that can be useful if you have many functions in your class that you'd like to enable only if MyProperty::value is true is to put all those functions in a base class and inherit from that class conditionally using CRTP.

    struct empty {};
    
    template<class T>
    struct float_funcs {
        T& Self() { return *static_cast<T*>(this); }
        const T& Self() const { return *static_cast<const T*>(this); }
    
        // put all functions depending on the MyProperty trait being true here:
        int f(int x) const {
            return x + Self().foo; // `foo` is accessible since we're a friend
        }
    };
    
    template<class T> // added helper variable
    inline constexpr bool MyProperty_v = MyProperty<T>::value;
    
    // inherit from float_funcs<S<T>> if the condition is `true` or `empty` otherwise
    template<class T>
    struct S : std::conditional_t<MyProperty_v<T>, float_funcs<S<T>>, empty> {
    private:
        friend std::conditional_t<MyProperty_v<T>, float_funcs<S<T>>, empty>;
        int foo = 1;
    };
    

    With this you don't need SFINAE away f and you'll get a clear compilation error if you try to use f when MyProperty<T> is not fulfilled. f doesn't even exist in any form in that case.