Search code examples
c++c++20c++-conceptstemplate-argument-deductionparameter-pack

How to extract requires clause with two parameter packs into a concept?


I have this (supposedly not so useful) class template with templated constructor which is a candidate for perfect forwarding. I, however, wanted to make sure that the types passed to the constructor are the exact same as the ones specified for the whole class (without cvref qualifiers):

template <typename... Ts>
struct foo {
    template <typename... CTs>
    requires (std::same_as<std::remove_cvref_t<Ts>, std::remove_cvref_t<CTs>> && ...)
    foo(CTs&& ...) {
        std::cout << "called with " << (sizeof...(CTs)) << " args\n";
    }
};

Now I can make:

auto f = foo<int, int>(1, 1);

and I can't make:

auto f = foo<float, int>(1, 1);

which is good.


But I wanted to extract the requires ... body to a concept:

template <typename... T1s, typename... T2s>
concept same_unqualified_types = (
        std::same_as<
                std::remove_cvref_t<T1s>,
                std::remove_cvref_t<T2s>
        > && ...
);

template <typename... Ts>
struct foo {
    template <typename... CTs>
    requires same_unqualified_types<Ts..., CTs...>
    foo(CTs&& ...) {                // 16
        std::cout << "called with " << (sizeof...(CTs)) << " args\n";
    }
};

int main() {
    auto f = foo<int, int>(1, 1);   // 22
}

But this gives me this error:

main.cpp: In function 'int main()': 
main.cpp:22:32: error: no matching function for call to 'foo<int, int>::foo(int, int)'
22 |     auto f = foo<int, int>(1, 1);
   |   
main.cpp:16:5: note: candidate: 'template<class ... CTs>  requires  same_unqualified_types<Ts ..., CTs ...> foo<Ts>::foo(CTs&& ...) [with CTs = {CTs ...}; Ts = {int, int}]'
16 |     foo(CTs&& ...) {
   |     ^~~
main.cpp:16:5: note:   template argument deduction/substitution failed:
main.cpp:16:5: note: constraints not satisfied
main.cpp: In substitution of 'template<class ... CTs>  requires  same_unqualified_types<Ts ..., CTs ...> foo<int, int>::foo(CTs&& ...) [with CTs = {int, int}]':
main.cpp:22:32:   required from here
main.cpp:5:9:   required for the satisfaction of 'same_unqualified_types<Ts ..., CTs ...>' [with CTs = {int, int}; Ts = {int, int}]
main.cpp:22:32: error: mismatched argument pack lengths while expanding 'same_as<typename std::remove_cvref<T1s>::type, typename std::remove_cvref<T2s>::type>'
22 |     auto f = foo<int, int>(1, 1);
   |    

I suppose I might be doing something wrong with the concept same_unqualified_types where I'm trying to have two parameter packs. I've tried to test it manually, but it doesn't seem to work, even if I do same_unqualified_types<int, int> or same_unqualified_types<int, int, Pack...>, where Pack... is a parameter pack of two ints.

Where is my logic flawed? Can I extract that requires clause to a concept?

Disclaimer: I know I can achieve something similar with CTAD and deduction guides - without even needing any concepts. I just wish to know where my understanding is flawed.


Solution

  • Concepts don't get special privileges with regard to template parameter packs. You can't have two packs in a set of template parameters unless there's something to distinguish them in terms of the arguments passed to them (one could be a series of types while the other is a series of values, or you have access to template argument deduction to differentiate them, or something like that).

    The best you're going to be able to do is to create an unqualified equivalent of same_as and just use that with pack expansion as needed:

    
    template<typename T, typename U>
    concept same_as_unqual = std::same_as<<std::remove_cvref_t<T>, <std::remove_cvref_t<U>>;
    
    template <typename... Ts>
    struct foo {
        template <typename... CTs>
        requires (same_as_unqual<Ts, CTs> && ...)
        ...
    

    A single concept that does the pair-wise comparison between the parameters just isn't possible. Well, it's possible, but it'd be a lot more verbose, as you would likely need to bundle them in some kind of type-list. I don't think same_as_all<type_list<Ts...>, type_list<Us...>> is better than doing an explicit expansion.