Using SFINAE partial specialization without touching the primary template
Asked Answered
N

3

9

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).

Nereus answered 9/8, 2016 at 20:23 Comment(1)
Depending on what actual forms of T you're interested in, this might help: https://mcmap.net/q/377537/-using-sfinae-for-template-class-specialisationKoto
B
4

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.

Bastien answered 9/8, 2016 at 21:5 Comment(2)
Thanks a lot for the explanation. It seems that macros are the way to go for my specific problem, but the interesting general problem of specializing using a trait doesn't seem to have a solution.Nereus
Could foo somehow be defined with SFINAE so that the specialization of S<foo<T>> is only defined when foo meets its own SFINAE criteria?Devanagari
E
5

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';
}
Endlong answered 25/11, 2021 at 15:18 Comment(0)
B
4

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.

Bastien answered 9/8, 2016 at 21:5 Comment(2)
Thanks a lot for the explanation. It seems that macros are the way to go for my specific problem, but the interesting general problem of specializing using a trait doesn't seem to have a solution.Nereus
Could foo somehow be defined with SFINAE so that the specialization of S<foo<T>> is only defined when foo meets its own SFINAE criteria?Devanagari
E
2

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...

Edwardoedwards answered 9/8, 2016 at 20:47 Comment(3)
As per updated question (and my initial suspicion), the float_or_int thing was just an example. The actual point is to specialize the struct for all types meeting a trait, so this does not answer the question.Bastien
Yes, the int/float specialization is just an example. I started to think about SFINAE because I realized that it would simplify the code a lot, beyond what I could achieve with the use of macros. In my case, I have multiple template parameters that need to be specialized, which means there's a bit of a combinatorial explosion.Nereus
Using a separate struct and inheriting from it when specializing cuts down on a lot of the code that would need to be written. Using this approach, the macro solution doesn't look too bad. Thanks a lot!Nereus

© 2022 - 2024 — McMap. All rights reserved.