using SFINAE for template class specialisation

 ̄綄美尐妖づ 提交于 2019-11-27 18:59:25

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.

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.

bcmpinc

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.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!