部分模板特化中匹配可变参数模板模板参数的精确规则

Jak*_*ark 7 c++ templates partial-specialization template-templates language-lawyer

在为另一个问题创建这个答案时,我遇到了以下问题。考虑这个程序(godbolt):

#include <variant>
#include <iostream>

template <typename T>
struct TypeChecker {
    void operator()() {
        std::cout << "I am other type\n";
    }
};

template<typename... Ts, template<typename...> typename V>
requires std::same_as<V<Ts...>, std::variant<Ts...>>
struct TypeChecker<V<Ts...>>
{
    void operator()()
    {
        std::cout << "I am std::variant\n";
    }
};

int main()
{
    TypeChecker<std::variant<int, float>>{}();
    TypeChecker<int>{}();
}
Run Code Online (Sandbox Code Playgroud)

输出(也是预期的)如下(使用 clang 14.0.0 以及 gcc 12.1):

I am std::variant
I am other type
Run Code Online (Sandbox Code Playgroud)

然而,如果模板的参数列表中的三个点被删除,就像这样(整个程序运行在godbolt上):

template<typename... Ts, template<typename> typename V>
Run Code Online (Sandbox Code Playgroud)

,那么 clang 和 gcc 的输出是不同的。clang 14.0.0编译的程序输出

I am other type
I am other type
Run Code Online (Sandbox Code Playgroud)

而 gcc 12.1 编译的程序输出

I am std::variant
I am other type
Run Code Online (Sandbox Code Playgroud)

看来使用非可变模板 template 在 clang 和 gcc 中表现出不同的匹配规则。所以我的问题是,如果定义明确的话,哪种行为是正确的,为什么?

Yur*_*isk 5

由于采用了缺陷报告分辨率P0522R0,模板参数匹配不再需要精确匹配模板参数列表,按照标准正确输出为:

\n
I am std::variant\nI am other type\n
Run Code Online (Sandbox Code Playgroud)\n

在当前草案(还包含与 C++20 概念相关的更改)中,相关标准摘录为temp.arg.template#3-4(粗体强调是我的):

\n
\n
    \n
  1. 当 P 至少与模板参数 A 一样专用时,模板参数与模板参数 P匹配比较,如果 P 不受约束,则不考虑 A 上的约束。如果 P 包含模板参数包,则如果 A 的每个模板参数都与P 的模板头中的相应模板参数匹配,则 A 也匹配 P。如果两个模板参数属于同一类型(类型、非类型),则它们匹配。 -type, template),对于非类型template-parameter,它们的类型是等价的 ([temp.over.link]),对于模板template-parameter,它们对应的每个template-parameter都是递归匹配的。当 P 的template-head包含模板参数包([temp.variadic])时,该模板参数包将匹配零个或多个模板参数或A 的template-head中具有相同类型和形式的模板参数包: P中的模板参数包(忽略这些模板参数是否是模板参数包)。
  2. \n
  3. 模板模板参数P至少与模板模板参数A 一样专用,如果给定以下对两个函数模板的重写,根据以下公式,对应于 P 的函数模板至少与对应于 A 的函数模板一样专用函数模板的部分排序规则。给定一个发明的类模板 X,其模板头为 A(包括默认参数和requires-clause,如果有的话):\n
      \n
    • (4.1) 两个函数模板分别具有与 P 或 A 相同的模板参数和requires-clause(如果有)。
    • \n
    • (4.2) 每个函数模板都有一个函数形参,其类型是 X 的特化,模板实参对应于各个函数模板中的模板形参,其中,对于函数模板的模板头中的每个模板形参 PP 有一个对应的模板参数 AA 已形成。如果 PP 声明了一个模板参数包,那么 AA 就是包扩展 PP... ([temp.variadic]); 否则,AA 是id 表达式PP。\n如果重写产生无效类型,则 P 至少不如 A 那样专业化。
    • \n
    \n
  4. \n
\n
\n

因此,正如我们所看到的,现在仅在模板模板参数的参数列表包含包(如您的原始示例)的情况下才考虑精确(根据参数包的特殊规则)参数匹配,否则只有新的至少关系用于测试匹配,它为模板模板参数和参数定义各自的函数模板,并根据模板函数的部分排序规则测试参数引发的函数是否至少与参数引发的函数一样专业。

\n

特别是,将 A=std::variant与 P=匹配template<typename> typename V,我们得到这些相应的函数模板:

\n
template<typename...> class X;\ntemplate<typename T> void f(X<T>); // for P\ntemplate<typename... Ts> void f(X<Ts...>); // for A\n
Run Code Online (Sandbox Code Playgroud)\n

因此,为了证明 A 与 P 匹配,我们需要证明f(X<T>)至少与 一样专业f(X<Ts...>)temp.func.order#2-4说:

\n
\n
    \n
  1. 部分排序通过依次转换每个模板(请参阅下一段)并使用函数类型执行模板参数推导,选择两个函数模板中哪一个比另一个更专业。推导过程确定其中一个模板是否比另一个更专业。如果是这样,则部分订购过程会选择更专业的模板。如果两种推导都成功,则部分排序将选择受约束更严格的模板(如果存在),如下所示。
  2. \n
  3. 为了生成转换后的模板,对于每个类型、非类型或模板模板参数(包括其模板参数包),分别合成一个唯一的类型、值或类模板,并将其替换为函数类型中该参数的每次出现。模板。...
  4. \n
  5. 使用转换后的函数模板的函数类型,对另一个模板执行类型推导,如 [temp.deduct.partial] 中所述。
  6. \n
\n
\n

f(X<T>)isf(X<U1>)和 of f(X<Ts...>)is的转换模板f(X<U2FromPack>),其中U1U2FromPack是两个合成的独特类型。现在,temp.deduct.partial#2-4,8,10说:

\n
\n
    \n
  1. 使用两组类型来确定部分排序。对于涉及的每个模板,都有原始函数类型和转换后的函数类型。\n[注 1:转换类型的创建在 [temp.func.order] 中描述。\xe2\x80\x94 尾注]\n推导过程使用转换后的类型作为参数模板,并使用另一个模板的原始类型作为参数模板。对于部分排序比较中涉及的每种类型,此过程会执行两次:一次使用转换后的 template-1 作为参数模板,使用转换后的 template-2 作为参数模板,再次使用转换后的 template-2 作为参数模板和 template-1作为参数模板。
  2. \n
  3. 用于确定排序的类型取决于完成部分排序的上下文:\n
      \n
    • (3.1) 在函数调用的上下文中,使用的类型是函数调用具有参数的函数参数类型。130
    • \n
    • (3.2) 在调用转换函数的上下文中,使用转换函数模板的返回类型。
    • \n
    • (3.3)在其他上下文中,使用函数模板的函数类型
    • \n
    \n
  4. \n
  5. 上面从参数模板中指定的每个类型以及参数模板中的相应类型都用作 P 和 A 的类型。
  6. \n
\n
\n
\n
    \n
  1. 使用结果类型 P 和 A,然后按照 [temp.deduct.type] 中的描述进行推导。...如果给定类型的推导成功,则认为参数模板中的类型至少与参数模板中的类型一样专业。
  2. \n
\n
\n
\n
    \n
  1. 如果对于用于确定排序的每对类型,F 中的类型至少与 G 中的类型一样专业,则函数模板 F 至少与函数模板G 一样专业。如果 F 位于至少与 G 一样专业化,而 G 至少不如 F 一样专业化。
  2. \n
\n
\n

现在,在我们的例子中,根据 3.3,函数类型本身是确定排序的类型中唯一考虑的类型。因此,根据 2、8 和 10,要知道 是否f(X<T>)至少与 一样专业化,f(X<Ts...>)我们需要查看是否void(X<T>)至少与 一样专业化void(X<Ts...>),或者等效地,从 A=的 P=类型推导是否成功。它根据temp.deduct.type#9-10执行:void(X<Ts...>)void(X<U1>)

\n
\n
    \n
  1. 如果 P 具有包含 <T> 或 <i> 的形式,则将P 的相应模板参数列表的每个参数 P i \n 与 A 的相应模板参数列表的相应参数 A i进行比较。如果模板参数P 的列表包含不是最后一个模板参数的包扩展,整个模板参数列表是非推导的上下文。如果 P i是包扩展,则将 P i的模式与 A 的模板参数列表中的每个剩余参数进行比较。每次比较都会推导出由 P i扩展的模板参数包中后续位置的模板参数。...
  2. \n
  3. 类似地,如果 P 具有包含 (T) 的形式,则将P 的各个参数类型列表 ([dcl.fct]) 的每个参数类型 P i与相应参数的相应参数类型 A i进行比较 - A 的类型列表...
  4. \n
\n
\n

X<Ts...>这里,每 10 次,函数类型的比较都会导致和的一次比较X<U1>,根据 9,该比较会成功。

\n

因此,演绎是成功的,因此f(X<T>)确实至少与 一样专业f(X<Ts...>),因此std::variant匹配template<typename> typename V。直观上,我们给出了“更通用”的模板模板参数,它应该可以很好地满足更具体的模板模板参数的预期用途。

\n

实际上,不同的编译器在不同的情况下启用 P0522R0 的更改,并且cppreference模板参数页面(模板模板参数部分)包含有关 GCC、Cland 和 MSVC 的链接和信息。特别是,C++17+ 模式下的 GCC 默认启用它(对于以前带有编译器标志的标准fnew-ttp-matching),但除非提供了标志,否则 Clang 在任何模式下都不会-frelaxed-template-template-args启用它,因此您会得到它们的输出差异。有了该标志,Clang 也会产生正确的行为(godbolt)。

\n