using SFINAE for template class specialisation

2019-01-13 20:20发布

问题:

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

回答1:

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.


回答2:

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.



回答3:

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.