Reputation: 595
I would like to define a template class MyClass
where the template parameters must be specializations of specific template classes. In this example, I would like MyClass
to be a template class that accepts for its first template parameter a specialization of A
and for its second template parameter a specialization of B
. I may then instantiate this class as follows:
MyClass<A<int, int>, B<double, int>>;
Now, you might say, "just take the arguments of A
and B
as the template parameters of MyClass
". Unfortunately, in my situation, A
and B
are both variadic templates, and so it won't be possible in the parameter list for MyClass
to find the end of the template arguments for A
and the beginning of the template arguments for B
.
Examples of A
and B
follow:
template <typename... Args>
class A{
public:
A(){}
};
template <typename... Args>
class B{
public:
B(){}
};
I have found older answers that provide solutions like using SFINAE. My understanding is that, unfortunately, SFINAE has drawbacks like disabling type deduction; I tend to avoid it. My question is this: are there newer language features that allow me to get the behavior I want without SFINAE?
To give you a bit better idea of what I am imagining for MyClass
, I provide this bad example below:
template <template<typename... Args> class A, typename... Args1, template<typename... Args> class B, typename... Args2>
class MyClass { // use A<Args1...> and B<Args2...> in here
public:
MyClass() {}
}; // fails because template parameter pack Args1 is not at the end of the parameter list
Upvotes: 0
Views: 1552
Reputation: 2564
An alternative to SFINAE are C++20 constraints and concepts. In your case - regardless of SFINAE or concepts - the key problem will be the template parameter packs, as the standard dictates
A template parameter pack of a function template shall not be followed by another template parameter unless that template parameter can be deduced from the parameter-type-list ([dcl.fct]) of the function template or has a default argument ([temp.deduct]). A template parameter of a deduction guide template ([temp.deduct.guide]) that does not have a default argument shall be deducible from the parameter-type-list of the deduction guide template.
This means that you can't have something like
template <template<typename...> class A, typename... Args1, template<typename...> class B, typename... Args2>
class MyClass {
public:
MyClass() {}
};
directly.
One possible solution is to not have a template template class at all and make sure implicitly (e.g. by a function call to a properly overloaded function) that the template arguments are correct. You could actually formalise @cigien's approach with concepts and further remove the requirement of default-constructability by using std::declval
:
Introduce two functions that can only be called by objects of type A
and B
respectively:
template <typename... Args>
void a(A<Args...>) {}
template <typename... Args>
void b(B<Args...>) {}
Define two concepts which require that an overload of the corresponding function exists that can be called with a value of the corresponding type by using std::declval
instead of the default-constructor:
template <typename T>
concept is_A = requires { a(std::declval<T>()); };
template <typename T>
concept is_B = requires { b(std::declval<T>()); };
Finally enforce the corresponding concepts on your template parameters as follows:
template <is_A A, is_B B>
class MyClass {
public:
MyClass() {}
};
With this approach the class can be default-constructed with
MyClass<A<int,int>, B<double,int>> my_class{};
Contrary to @cigien's version this does not require A
and B
to be default-constructible!
Depending on your planned usage you could also use the template argument deduction when the constructor is called but be aware that then the class itself will not distinguish between different variadic template arguments (meaning std::is_same
will actually return true for two different parameter packs), only the constructor will. Furthermore the class won't be default constructable anymore and you would have to call it with
MyClass my_class{A<int,int>{}, B<int,double>{}};
One way would be to not template the class at all and template the constructor with two template parameter packs
class MyClass {
public:
template <typename... Args1, typename... Args2>
MyClass(A<Args1...>, B<Args2...>) {}
};
You could make this more flexible by introducing the concepts
template <template<typename...> class T, typename... Ts>
concept is_A = std::is_same_v<A<Ts...>,T<Ts...>>;
template <template<typename...> class T, typename... Ts>
concept is_B = std::is_same_v<B<Ts...>,T<Ts...>>;
(or alternatively with std::is_base_of
instead), templating the class without specifying the variadic template parameters and then making the constructor templated as well with two template parameter packs that can be deducted from the input arguments
template <template<typename...> class C1, template<typename...> class C2>
requires is_A<C1> && is_B<C2>
class MyClass {
public:
template <typename... Args1, typename... Args2>
requires is_A<C1,Args1...> && is_B<C2,Args2...>
MyClass(C1<Args1...>, C2<Args2...>) {}
};
and add a requires clause to the class and/or constructor.
The given constraints could of course also be enforced with SFINAE std::enable_if_t
instead of concepts. For an example of this see e.g. here.
Upvotes: 2
Reputation: 60422
I don't believe this is possible by specializing class templates, but you can work around this by using function template argument deduction, which can distinguish between multiple parameter packs
template <typename... Args1, typename... Args2>
void f(A<Args1...>, B<Args2...>) {}
This function will be matched when passed instantiations of A
and B
, which we use as the true case.
For the false case, we can just match anything and delete it
template <typename... Ts>
void f(Ts...) = delete;
Now, you can have MyClass
take 2 type parameters, and call f
, say in the constructor, to make sure the parameters are instantiations of A
and B
.
template<typename T, typename U>
class MyClass {
public:
MyClass()
{
f(T{}, U{}); // hard error if not called with specializations of A and B
}
};
This will essentially assert that the types satisfy the constraints you want, but you can further extract the arguments of A
and B
if you want to use them as well.
Here's a demo.
Upvotes: 1