Search code examples
c++templateslanguage-lawyerc++20parameter-pack

std::type_identity to support several variadic argument lists


std::type_identity can be used to provide non-deducible context. So, I wondered if it will work on a deduced variadic list. But different compilers give different results.
https://godbolt.org/z/4cfqbxdeo

#include <type_traits>
 
struct in_between{};

template <typename... T>
struct args_tag
{
    using type = std::common_type_t<T...>;
};

template <typename... T>
void bar(args_tag<T...>, std::type_identity_t<T>..., int, std::type_identity_t<T>...) {}

template <typename... T>
void bar(args_tag<T...>, std::type_identity_t<T>..., in_between, std::type_identity_t<T>...) {}

// example
int main() {
    bar(args_tag<int, int>{}, 4, 8, 15, 16, 23);
    bar(args_tag<int, int>{}, 4, 8, in_between{}, 16, 23);
}

The first one compiles with gcc and msvc.

bar(args_tag<int, int>{}, 4, 8, 15, 16, 23);

The second one compiles only with msvc.

bar(args_tag<int, int>{}, 4, 8, in_between{}, 16, 23);

What should be the behavior according to the standard?


Solution

  • The program is well-formed for both the calls for the following reason(s). In the given example, deduction is performed only using the first function parameter args_tag<T...>. The remaining two parameter packs won't be involved in deduction since they are in non-deduced context. After the deduction is performed using the first parameter args_tag<T...>, the deduced T is substituted into the remaining two parameter packs as per temp.deduc.general#3. This means that for the call bar(args_tag<int, int>{}, 4, 8, 15, 16, 23), T gets deduced as {int, int}. Now for each of the args in this list {int, int}, we get a corresponding std::type_identity parameter(4 in total because there are two std::type_identity parameter and T has two ints) as shown in the below declaration. Essentially, the declaration becomes:

    template <>
    //--------------------------vvv--vvv--->deduced from first argument
    void bar<int, int>(args_tag<int, int>,
    //-----------------vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv-->gets fixed/substituted when first argument is supplied 
                       std::type_identity_t<int>,std::type_identity_t<int>,
                       int, 
    //-----------------vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv-->gets fixed/substituted when first argument is supplied 
                       std::type_identity_t<int>,std::type_identity_t<int>) {}
    
    

    Now as we can see, this is viable for the call bar(args_tag<int, int>{}, 4, 8, 15, 16, 23) and so should succeed.

    This can be seen from temp.deduct.general#3:

    When all template arguments have been deduced or obtained from default template arguments, all uses of template parameters in the template parameter list of the template are replaced with the corresponding deduced or default argument values. If the substitution results in an invalid type, as described above, type deduction fails. If the function template has associated constraints ([temp.constr.decl]), those constraints are checked for satisfaction ([temp.constr.constr]). If the constraints are not satisfied, type deduction fails. In the context of a function call, if type deduction has not yet failed, then for those function parameters for which the function call has arguments, each function parameter with a type that was non-dependent before substitution of any explicitly-specified template arguments is checked against its corresponding argument; if the corresponding argument cannot be implicitly converted to the parameter type, type deduction fails.

    (emphasis mine)


    That is, gcc and clang are wrong in giving a diagnostic. See also the contrived example at the end to make this more clear.


    This also explains why a call such as bar(args_tag<int, int, double>{}, 4, 8, 15, 16, 23, 34,34); passes while the call bar(args_tag<int, int, double>{}, 4, 8, 15, 16, 23, 34) fails in all compilers.

    bar(args_tag<int, int, double>{}, 4, 8, 15, 16, 23, 34, 34); //valid because `T={int, int, double}` means the second and fourth packs can each get three arguments 
    bar(args_tag<int, int, double>{}, 4, 8, 15, 16, 23, 34); //fails
    

    Contrived Example

    To make the above explanation more clear, below is a contrived example showing the logic/reasoning. Note that the example illustrates what should be happening in your program(both calls should compile).

    #include <type_traits>
     
    template <typename... T>
    struct args_tag{  };
    
    template <typename... T>
    void bar(args_tag<T...>, std::type_identity_t<T>...) {}
    
    // example
    int main() {
        bar(args_tag<int, int>{}, 4, 8); //valid because T={int, int} and exactly two extra  args are provided
        bar(args_tag<int, int, int>{}, 4, 8, 6); //valid because T={int, int, int} and exactly three extra args are provided
        //bar(args_tag<int, int>{}, 4);  //invalid because because T={int, int} but only one extra arg is provided 
    }
    

    Demo


    GCC rejects valid program involving parameter packs with in between class type

    Clang rejects valid program involving parameter packs with in between type