mana
mana

Reputation: 595

How do I require template parameters to be specializations of a specific template class?

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

Answers (2)

2b-t
2b-t

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!

    Try it here


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...>) {}
    };
    

    Try it here

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

    Try it here.

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

cigien
cigien

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

Related Questions