std::type_identity 支持多个可变参数列表

Ser*_*nik 6 c++ templates language-lawyer c++20 parameter-pack

std::type_identity可用于提供不可推论的上下文。所以,我想知道它是否适用于推导的可变参数列表。但不同的编译器给出不同的结果。
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);
}
Run Code Online (Sandbox Code Playgroud)

第一个使用 gcc 和 msvc 编译。

bar(args_tag<int, int>{}, 4, 8, 15, 16, 23);
Run Code Online (Sandbox Code Playgroud)

第二个只能用 msvc 编译。

bar(args_tag<int, int>{}, 4, 8, in_between{}, 16, 23);
Run Code Online (Sandbox Code Playgroud)

根据标准,行为应该是什么?

use*_*570 1

由于以下原因,该程序对于这两个调用都是格式良好的。在给定的示例中,仅使用第一个函数参数 执行推导args_tag<T...>。其余两个参数包不参与推导,因为它们处于非推导上下文中。使用第一个参数执行推导后,将推导args_tag<T...>结果代入其余两个参数包,按照temp.deduc.general#3。这意味着对于调用,被推导为。现在,对于这个列表中的每个 args ,我们得到一个相应的参数(总共 4 个,因为有两个参数并且有两个s),如下面的声明所示。本质上,声明变成:Tbar(args_tag<int, int>{}, 4, 8, 15, 16, 23)T {int, int}{int, int}std::type_identitystd::type_identityTint

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

Run Code Online (Sandbox Code Playgroud)

现在我们可以看到,这对于调用来说是可行的bar(args_tag<int, int>{}, 4, 8, 15, 16, 23),因此应该会成功。

这可以从temp.deduct.general#3中看出:

当所有模板实参都已从默认模板实参推导或获取时,模板的模板参数列表中所有使用的模板参数都将替换为相应的推导或默认实参值。如果替换导致无效类型,如上所述,类型推导将失败。如果函数模板具有关联的约束 ([temp.constr.decl]),则检查这些约束是否满足 ([temp.constr.constr])。如果不满足约束,类型推导就会失败。在函数调用的上下文中,如果类型推导尚未失败,则对于函数调用具有参数的那些函数参数,在替换任何显式指定的模板参数之前具有非依赖类型的每个函数参数是检查其相应的参数;如果对应的实参无法隐式转换为形参类型,则类型推导失败。

(强调我的)


也就是说,gcc 和 clang 给出的诊断是错误的。另请参阅最后的人为示例,以使其更加清楚。


这也解释了为什么在所有编译器中诸如这样的调用都会bar(args_tag<int, int, double>{}, 4, 8, 15, 16, 23, 34,34);通过而调用bar(args_tag<int, int, double>{}, 4, 8, 15, 16, 23, 34)会失败。

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
Run Code Online (Sandbox Code Playgroud)

人为的例子

为了使上述解释更清楚,下面是一个展示逻辑/推理的人为示例。请注意,该示例说明了程序中应该发生的情况(两个调用都应该编译)。

#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 
}
Run Code Online (Sandbox Code Playgroud)

演示


GCC 拒绝涉及类间类型参数包的有效程序

Clang 拒绝涉及中间类型参数包的有效程序