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.
static_assert
version wouldn't work. Care to elaborate? – WalletUser<>
for anyData<>
or subclass, but not for any other type? ShouldUser<int>
fail to compile? – GravelyUser<int>
). – SulfonateUser<some type dervied from OtherData<0>>
(withtemplate<int> struct OtherData
). – Sulfonatestd::enable_if
for this kind of SFINAE is a bit roundabout. Usingtypename = std::true_type
as the defaulted parameter you can write partial specs that matches<T, typename is_foo<T>::type>
assumingis_foo
is e.g. a UnaryTypeTrait in the sense of the Standard (any of the traits from<type_traits>
work like that). – Vanillic