using SFINAE for template class specialisation
Asked Answered
S

3

44

suppose I have these declarations

template<typename T> class User;
template<typename T> class Data;

and want to implement User<> for T = Data<some_type> and any class derived from Data<some_type> but also allow for other specialisations defined elsewhere.

If I didn't already have the declaration of the class template User<>, I could simply

template<typename T,
         typename A= typename std::enable_if<is_Data<T>::value>::type>
class User { /*...*/ };

where

template<template<typename> data>> struct is_Data
{ static const bool value = /* some magic here (not the question) */; };

However, this has two template parameters and thus clashes with the previous declaration, where User<> is declared with only one template parameter. Is there anything else I can do?

(Note

template<typename T,
         typename A= typename std::enable_if<is_Data<T>::value>::type>
class User<T> { /*...*/ };

doesn't work (default template arguments may not be used in partial specializations), nor does

template<typename T> class User<Data<T>> { /*...*/ };

as it doesn't allow types derived from Data<>, neither does

template<typename T>
class User<typename std::enable_if<is_Data<T>::value,T>::type>
{ /*...*/ };

since template parameter T is not used in partial specialization.)

Sulfonate answered 12/10, 2012 at 12:14 Comment(10)
SFINAE can be applied to pick template specialisations, see en.cppreference.com/w/cpp/types/enable_ifSulfonate
So it can! I learned something.Ivey
I don't think I understand why the static_assert version wouldn't work. Care to elaborate?Wallet
@Wallet that was not correct code anyway (didn't specialise the original class template; removed now).Sulfonate
Just to clarify: You want to be able to instatiate User<> for any Data<> or subclass, but not for any other type? Should User<int> fail to compile?Gravely
@JohnDibling Yes exactly (unless somebody else somewhere declared a specialisation of User<int>).Sulfonate
@Sulfonate In that case, why bother with a partial specialisation at all? Can't you use a static assert in the non-specialised version?Gamopetalous
@hvd NO I want to have other partial specialisations, for example User<some type dervied from OtherData<0>> (with template<int> struct OtherData).Sulfonate
Is the issue that you can't change User, or you're looking for a way to avoid that. And if you're looking for a way to avoid it, but it's possible to change it, as long it's something that didn't break your already code be acceptable?Almuce
Note that using std::enable_if for this kind of SFINAE is a bit roundabout. Using typename = std::true_type as the defaulted parameter you can write partial specs that matches <T, typename is_foo<T>::type> assuming is_foo is e.g. a UnaryTypeTrait in the sense of the Standard (any of the traits from <type_traits> work like that).Vanillic
M
11

Since you said you were still waiting for a better answer, here's my take on it. It's not perfect, but I think it gets you as far as possible using SFINAE and partial specializations. (I guess Concepts will provide a complete and elegant solution, but we'll have to wait a bit longer for that.)

The solution relies on a feature of alias templates that was specified only recently, in the standard working drafts after the final version of C++14, but has been supported by implementations for a while. The relevant wording in draft N4527 [14.5.7p3] is:

However, if the template-id is dependent, subsequent template argument substitution still applies to the template-id. [ Example:

template<typename...> using void_t = void;
template<typename T> void_t<typename T::foo> f();
f<int>(); // error, int does not have a nested type foo

—end example ]

Here's a complete example implementing this idea:

#include <iostream>
#include <type_traits>
#include <utility>

template<typename> struct User { static void f() { std::cout << "primary\n"; } };

template<typename> struct Data { };
template<typename T, typename U> struct Derived1 : Data<T*> { };
template<typename> struct Derived2 : Data<double> { };
struct DD : Data<int> { };

template<typename T> void take_data(Data<T>&&);

template<typename T, typename = decltype(take_data(std::declval<T>()))> 
using enable_if_data = T;

template<template<typename...> class TT, typename... Ts> 
struct User<enable_if_data<TT<Ts...>>> 
{ 
    static void f() { std::cout << "partial specialization for Data\n"; } 
};

template<typename> struct Other { };
template<typename T> struct User<Other<T>> 
{ 
    static void f() { std::cout << "partial specialization for Other\n"; } 
};

int main()
{
    User<int>::f();
    User<Data<int>>::f();
    User<Derived1<int, long>>::f();
    User<Derived2<char>>::f();
    User<DD>::f();
    User<Other<int>>::f();
}

Running it prints:

primary
partial specialization for Data
partial specialization for Data
partial specialization for Data
primary
partial specialization for Other

As you can see, there's a wrinkle: the partial specialization isn't selected for DD, and it can't be, because of the way we declared it. So, why don't we just say

template<typename T> struct User<enable_if_data<T>> 

and allow it to match DD as well? This actually works in GCC, but is correctly rejected by Clang and MSVC because of [14.5.5p8.3, 8.4] ([p8.3] may disappear in the future, as it's redundant - CWG 2033):

  • The argument list of the specialization shall not be identical to the implicit argument list of the primary template.
  • The specialization shall be more specialized than the primary template (14.5.5.2).

User<enable_if_data<T>> is equivalent to User<T> (modulo substitution into that default argument, which is handled separately, as explained by the first quote above), thus an invalid form of partial specialization. Unfortunately, matching things like DD would require, in general, a partial specialization argument of the form T - there's no other form it can have and still match every case. So, I'm afraid we can conclusively say that this part cannot be solved within the given constraints. (There's Core issue 1980, which hints at some possible future rules regarding the use of template aliases, but I doubt they'll make our case valid.)

As long as the classes derived from Data<T> are themselves template specializations, further constraining them using the technique above will work, so hopefully this will be of some use to you.


Compiler support (this is what I tested, other versions may work as well):

  • Clang 3.3 - 3.6.0, with -Wall -Wextra -std=c++11 -pedantic - works as described above.
  • GCC 4.7.3 - 4.9.2, same options - same as above. Curiously, GCC 5.1.0 - 5.2.0 no longer selects the partial specialization using the correct version of the code. This looks like a regression. I don't have time to put together a proper bug report; feel free to do it if you want. The problem seems to be related to the use of parameter packs together with a template template parameter. Anyway, GCC accepts the incorrect version using enable_if_data<T>, so that can be a temporary solution.
  • MSVC: Visual C++ 2015, with /W4, works as described above. Older versions don't like the decltype in the default argument, but the technique itself still works - replacing the default argument with another way of expressing the constraint makes it work on 2013 Update 4.
Miles answered 22/6, 2015 at 22:46 Comment(0)
S
27

IF the original declaration of User<> can be adapted to

template<typename, typename=std::true_type> class User;

then we can find a solution (following Luc Danton's comment, instead of using std::enable_if)

template<typename>
struct is_Data : std::false_type {};
template<typename T>
struct is_Data<Data<T>> : std::true_type {};

template<typename T>
class User<T, typename is_Data<T>::type >
{ /* ... */ };

However, this doesn't answer the original question, since it requires to change the original definition of User. I'm still waiting for a better answer. This could be one that conclusively demonstrates that no other solution is possible.

Sulfonate answered 12/10, 2012 at 13:9 Comment(4)
The solution is demonstrated correctly in the link posted, however it was not transferred/adapted correctly in this answer - it should be a partial specialization like the following: template<typename T> class User<T, typename std::enable_if<is_Data<T>::value>::type> { ... }; ... will post an "edit".Vagabond
I think this answer could be adapted to do sfinae without modifying original definitionSupplementary
@Supplementary Interesting idea. I tried, but couldn't get it to work. The situation is similar to the last failed example in the question (when the compiler warns me partial specialization contains a template parameter that cannot be deduced; this partial specialization will never be used), but instead I get no warning but also it's not picking up the specialization as desired. If you could get it to work, that would be great.Sulfonate
@Sulfonate This is weird, code like this works on gcc 5+ version but clang/gcc 4.9/msvc reject it along the lines that specialization is not actually specializing anything. I haven't figured out how to emulate fake specialization which will work for now, but it leads to super weird conclusion that you can sfinae like this only when you also specialzying class somehow. Here's one possible solution with weird indirectionSupplementary
M
11

Since you said you were still waiting for a better answer, here's my take on it. It's not perfect, but I think it gets you as far as possible using SFINAE and partial specializations. (I guess Concepts will provide a complete and elegant solution, but we'll have to wait a bit longer for that.)

The solution relies on a feature of alias templates that was specified only recently, in the standard working drafts after the final version of C++14, but has been supported by implementations for a while. The relevant wording in draft N4527 [14.5.7p3] is:

However, if the template-id is dependent, subsequent template argument substitution still applies to the template-id. [ Example:

template<typename...> using void_t = void;
template<typename T> void_t<typename T::foo> f();
f<int>(); // error, int does not have a nested type foo

—end example ]

Here's a complete example implementing this idea:

#include <iostream>
#include <type_traits>
#include <utility>

template<typename> struct User { static void f() { std::cout << "primary\n"; } };

template<typename> struct Data { };
template<typename T, typename U> struct Derived1 : Data<T*> { };
template<typename> struct Derived2 : Data<double> { };
struct DD : Data<int> { };

template<typename T> void take_data(Data<T>&&);

template<typename T, typename = decltype(take_data(std::declval<T>()))> 
using enable_if_data = T;

template<template<typename...> class TT, typename... Ts> 
struct User<enable_if_data<TT<Ts...>>> 
{ 
    static void f() { std::cout << "partial specialization for Data\n"; } 
};

template<typename> struct Other { };
template<typename T> struct User<Other<T>> 
{ 
    static void f() { std::cout << "partial specialization for Other\n"; } 
};

int main()
{
    User<int>::f();
    User<Data<int>>::f();
    User<Derived1<int, long>>::f();
    User<Derived2<char>>::f();
    User<DD>::f();
    User<Other<int>>::f();
}

Running it prints:

primary
partial specialization for Data
partial specialization for Data
partial specialization for Data
primary
partial specialization for Other

As you can see, there's a wrinkle: the partial specialization isn't selected for DD, and it can't be, because of the way we declared it. So, why don't we just say

template<typename T> struct User<enable_if_data<T>> 

and allow it to match DD as well? This actually works in GCC, but is correctly rejected by Clang and MSVC because of [14.5.5p8.3, 8.4] ([p8.3] may disappear in the future, as it's redundant - CWG 2033):

  • The argument list of the specialization shall not be identical to the implicit argument list of the primary template.
  • The specialization shall be more specialized than the primary template (14.5.5.2).

User<enable_if_data<T>> is equivalent to User<T> (modulo substitution into that default argument, which is handled separately, as explained by the first quote above), thus an invalid form of partial specialization. Unfortunately, matching things like DD would require, in general, a partial specialization argument of the form T - there's no other form it can have and still match every case. So, I'm afraid we can conclusively say that this part cannot be solved within the given constraints. (There's Core issue 1980, which hints at some possible future rules regarding the use of template aliases, but I doubt they'll make our case valid.)

As long as the classes derived from Data<T> are themselves template specializations, further constraining them using the technique above will work, so hopefully this will be of some use to you.


Compiler support (this is what I tested, other versions may work as well):

  • Clang 3.3 - 3.6.0, with -Wall -Wextra -std=c++11 -pedantic - works as described above.
  • GCC 4.7.3 - 4.9.2, same options - same as above. Curiously, GCC 5.1.0 - 5.2.0 no longer selects the partial specialization using the correct version of the code. This looks like a regression. I don't have time to put together a proper bug report; feel free to do it if you want. The problem seems to be related to the use of parameter packs together with a template template parameter. Anyway, GCC accepts the incorrect version using enable_if_data<T>, so that can be a temporary solution.
  • MSVC: Visual C++ 2015, with /W4, works as described above. Older versions don't like the decltype in the default argument, but the technique itself still works - replacing the default argument with another way of expressing the constraint makes it work on 2013 Update 4.
Miles answered 22/6, 2015 at 22:46 Comment(0)
B
6

As you only want to implement it when a single condition is true, the easiest solution is to use a static assertion. It does not require SFINAE, gives a clear compile error if used incorrectly and the declaration of User<> does not need to be adapted:

template<typename T> class User {
  static_assert(is_Data<T>::value, "T is not (a subclass of) Data<>");
  /** Implementation. **/
};

See also: When to use static_assert instead of SFINAE?. The static_assert is a c++11 construct, however there are plenty workarounds available for pre-c++11 compilers, like:

#define STATIC_ASSERT(consdition,name) \
  typedef char[(condition)?1:-1] STATIC_ASSERT_ ## name

If the declaration of user<> can be changed and you want two implementations depending on the value of is_Data, then there is also a solution that does not use SFINAE:

template<typename T, bool D=is_Data<T>::value> class User;

template<typename T> class User<T, true> {
  static_assert(is_Data<T>::value, "T is not (a subclass of) Data<>"); // Optional
  /* Data implementation */
};

template<typename T> class User<T, false> {
  static_assert(!is_Data<T>::value, "T is (a subclass of) Data<>"); // Optional
  /* Non-data implementation */
};

The static assertions only checks whether the user did not accidentally specify the template argument D incorrectly. If D is not specified explicitly, then the static assertions can be omitted.

Ballinger answered 19/2, 2014 at 20:14 Comment(1)
This does actually not solve the problem I had. I wanted still to allow for other specialisations of Data<T> (will edit the question to mention that).Sulfonate

© 2022 - 2024 — McMap. All rights reserved.