Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using SFINAE partial specialization without touching the primary template

I'm trying to specialize a struct template for multiple types at once using SFINAE. I know that something like the following works:

#include <iostream>

template <typename T, typename Enable = void>
struct S {
    void operator()() {
        std::cout << "Instantiated generic case" << std::endl;
    }
};

template<typename T>
using enabled_type = typename std::enable_if<
                         std::is_same<T, int>::value ||
                         std::is_same<T, float>::value
                     >::type;

template <typename T>
struct S<T, enabled_type<T>> {
    void operator()() {
        std::cout << "Instantiated int/float case" << std::endl;
    }
};

int main() {
    S<float>()();
    return 0;
}

My problem is that I can't modify the primary template of the S struct to add typename Enable = void, as it's part of an external header-only library. So the primary template will have to look like this:

template <typename T>
struct S {
    void operator()() {
        std::cout << "Instantiated generic case" << std::endl;
    }
};

Is there a way that I could still use SFINAE to specialize this template?

Edit: Note that the S struct is used by code in the external library, so I will have to actually specialize S and can't subclass it. Also, the real code I'm working on is much more complicated and would benefit much more from SFINAE than this simple example (I have multiple template parameters that need to be specialized for all combinations of a number of types).

like image 651
ibab Avatar asked Aug 09 '16 20:08

ibab


3 Answers

I have bad news for you: what you want is impossible. If the primary template does not have an extra template parameter that defaults to void for the purpose of doing enable_if, there's just no way to do this in the most general sense. The closest you can come is to specialize the struct for a template class itself, in other words:

template <class T>
struct foo {};

template <class T>
struct S<foo<T>> {};

This will work. But obviously this does not yield the same flexibility as specializing something iff it matches a trait.

Your problem is actually exactly equivalent to the problem of trying to specialize std::hash for any types satisfying a trait. Like in your problem, the primary class template definition cannot be changed as it's in library code, and the library code actually uses specializations of hash automatically internally in certain situations, so one cannot really do with defining a new template class.

You can see a similar question here: Specializing std::hash to derived classes. Inheriting from a class is a good example of something that can be expressed as a trait, but not as a templated class. Some pretty knowledgeable C++ folk had eyes on that question, and nobody provided a better answer than the OP's solution of writing a macro to stamp out specialization automatically. I think that that will be the best solution for you too, unfortunately.

In retrospect, hash perhaps should have been declared with a second template parameter that defaulted to void to support this use case with minimal overhead in other cases. I could have sworn I even saw discussion about this somewhere but I'm not able to track it down. In the case of your library code, you might try to file an issue to have them change that. It does not seem to be a breaking change, that is:

template <class T, class = void>
struct S {};

template <>
struct S<double> {};

seems to be valid.

like image 165
Nir Friedman Avatar answered Oct 05 '22 22:10

Nir Friedman


Are you sure the plain old specialization wouldn't suffice?

struct S_int_or_float {
    void operator()() {
        std::cout << "Instantiated int/float case" << std::endl;
    }
};

template<> struct S<int>: S_int_or_float {};
template<> struct S<float>: S_int_or_float {};

Edit: You say that you have to provide a combination of the parameters... So there are at least two of them... Lets see what we can do with that:

struct ParentS { /* some implementation */ }; 

template <class First, class Second>
struct S<First, Second, enable_if_t<trait_for_first_and_second<First, Second>::value, first_accepted_type_of_third_parameter> >: ParentS { };

template <class First, class Second>
struct S<First, Second, enable_if_t<trait_for_first_and_second<First, Second>::value, second_accepted_type_of_third_parameter> >: ParentS { };

// ...

It's not as good as if there would be additional parameter just for SFINAE but still not exponential...

like image 28
W.F. Avatar answered Oct 05 '22 23:10

W.F.


In C++20 you can use concepts to qualify template parameters.

On g++ 11.2 and clang 13.0, the following works as expected.

#include <iostream>

template <typename T> concept isok = std::is_floating_point_v<T>;
template <typename T> struct ob   { T field; ob(T t) : field(  t) { } };
template <isok T>     struct ob <T> { T field; ob(T t) : field(3*t) { } };
 
// Prints 9 9 27 27
int main() {
    std::cout << ob(9).field   << ' ' << ob('9').field  << ' '
              << ob(9.0).field << ' ' << ob(9.0f).field << '\n';
}
like image 22
smwatt Avatar answered Oct 05 '22 23:10

smwatt