模板偏序——为什么这里偏推成功

Template partial ordering - why does partial deduction succeed here

考虑以下简单的(在模板问题的范围内)示例:

#include <iostream>

template <typename T>
struct identity;

template <>
struct identity<int> {
    using type = int;
};

template<typename T> void bar(T, T ) { std::cout << "a\n"; }
template<typename T> void bar(T, typename identity<T>::type) { std::cout << "b\n"; }

int main ()
{
    bar(0, 0);
}

clang 和 gcc 都在那里打印 "a"。根据[temp.deduct.partial]和[temp.func.order]中的规则,为了确定偏序,我们需要合成一些独特的类型。所以我们有两次推导尝试:

+---+-------------------------------+-------------------------------------------+
|   | Parameters                    | Arguments                                 |
+---+-------------------------------+-------------------------------------------+
| a | T, typename identity<T>::type | UniqueA, UniqueA                          |
| b | T, T                          | UniqueB, typename identity<UniqueB>::type |
+---+-------------------------------+-------------------------------------------+

对于 "b" 的推导,根据 Richard Corden's answer,表达式 typename identity<UniqueB>::type 被视为一种类型,不会被计算。也就是说,这将被合成为:

+---+-------------------------------+--------------------+
|   | Parameters                    | Arguments          |
+---+-------------------------------+--------------------+
| a | T, typename identity<T>::type | UniqueA, UniqueA   |
| b | T, T                          | UniqueB, UniqueB_2 |
+---+-------------------------------+--------------------+

很明显"b"的推导失败了。这些是两种不同的类型,因此您不能将 T 推导出来。

但是,在我看来,A 的推导应该会失败。对于第一个参数,您将匹配 T == UniqueA。第二个参数是一个非推导的上下文——如果 UniqueA 可以转换为 identity<UniqueA>::type 那么推导不会成功吗?后者是替代失败,所以我也看不出这种推论是如何成功的。

在这种情况下,gcc 和 clang 如何以及为什么更喜欢 "a" 重载?

我认为关键在于以下陈述:

The second argument is a non-deduced context - so wouldn't that deduction succeed iff UniqueA were convertible to identity::type?

类型推导不执行 "conversions" 的检查。这些检查是使用真实的显式和推导参数作为重载决策的一部分进行的。

这是我对 select 要调用的函数模板所采取的步骤的总结(所有参考均取自 N3937,~ C++ '14):

  1. 替换显式参数并检查生成的函数类型是否有效。 (14.8.2/2)
  2. 执行类型推导并替换推导的结果参数。同样,结果类型必须有效。 (14.8.2/5)
  3. 在步骤 1 和 2 中成功的函数模板被专门化并包含在重载集中以进行重载解析。 (14.8.3/1)
  4. 转换序列通过重载解析进行比较。 (13.3.3)
  5. 如果两个函数特化的转换序列不是'better',则偏序算法用于查找更特化的函数模板。 (13.3.3)
  6. 偏序算法只检查类型推导是否成功。 (14.5.6.2/2)

编译器在第 4 步中已经知道在使用实参时可以调用两个特化。第 5 步和第 6 步用于确定哪个函数更专业。

正如评论中所讨论的,我相信函数模板偏序算法的几个方面在标准中是不清楚的或根本没有指定的,这在你的例子中有所体现。

为了让事情变得更有趣,MSVC(我测试了 12 和 14)拒绝了不明确的调用。我认为标准中没有任何内容可以最终证明哪个编译器是正确的,但我想我可能知道差异来自何处;下面有一条注释。

你的问题(和 this one)促使我对事情的运作方式进行更多调查。我决定写这个答案不是因为我认为它是权威的,而是为了将我找到的信息组织在一个地方(它不适合放在评论中)。希望对你有用。


首先,issue 1391 的提议决议。我们在评论和聊天中广泛讨论了它。我认为,虽然它确实提供了一些澄清,但它也引入了一些问题。它将 [14.8.2.4p4] 更改为(新文本以粗体显示):

Each type nominated above from the parameter template and the corresponding type from the argument template are used as the types of P and A. If a particular P contains no template-parameters that participate in template argument deduction, that P is not used to determine the ordering.

在我看来这不是一个好主意,原因如下:

  • 如果P是非依赖的,它根本不包含任何模板参数,所以它也不包含任何参与参数推导的参数,这将使粗体语句适用于它.但是,这会使 template<class T> f(T, int)template<class T, class U> f(T, U) 无序,这没有意义。这可以说是对措辞的解释问题,但它可能会引起混淆。
  • 它混淆了 用于确定顺序 的概念,这会影响 [14.8.2.4p11]。这使得 template<class T> void f(T)template<class T> void f(typename A<T>::a) 无序(从第一到第二推导成功,因为 T 没有用在根据新规则用于偏序的类型中,所以它可以保持没有价值)。目前,我测试过的所有编译器都报告第二个更专业。
  • 在下面的例子中,#2#1 更专业:

    #include <iostream>
    
    template<class T> struct A { using a = T; };
    
    struct D { };
    template<class T> struct B { B() = default; B(D) { } };
    template<class T> struct C { C() = default; C(D) { } };
    
    template<class T> void f(T, B<T>) { std::cout << "#1\n"; } // #1
    template<class T> void f(T, C<typename A<T>::a>) { std::cout << "#2\n"; } // #2
    
    int main()
    {
       f<int>(1, D());
    }
    

    #2 的第二个参数不用于偏序,因此从 #1#2 的推导成功,反之则不然)。目前,调用是模棱两可的,并且可以说应该保持模棱两可。


在查看了 Clang 对偏序算法的实现后,我认为可以通过以下方式更改标准文本以反映实际发生的情况。

保持 [p4] 不变,在 [p8] 和 [p9] 之间添加以下内容:

For a P / A pair:

  • If P is non-dependent, deduction is considered successful if and only if P and A are the same type.
  • Substitution of deduced template parameters into the non-deduced contexts appearing in P is not performed and does not affect the outcome of the deduction process.
  • If template argument values are successfully deduced for all template parameters of P except the ones that appear only in non-deduced contexts, then deduction is considered successful (even if some parameters used in P remain without a value at the end of the deduction process for that particular P / A pair).

备注:

  • 关于第二个要点:[14.8.2.5p1] 谈论查找模板参数值 ,这将使 P,在替换推导出的值之后(称之为推导出的 A), 与 A 兼容。这可能会导致对部分排序期间实际发生的事情感到困惑;没有替换。
  • MSVC 在某些情况下似乎没有实现第三个要点。有关详细信息,请参阅下一节。
  • 第二个和第三个要点旨在涵盖 P 具有 A<T, typename U::b> 等形式的情况,这些情况未包含在第 1391 期中的措辞中。

将当前[p10]更改为:

Function template F is at least as specialized as function template G if and only if:

  • for each pair of types used to determine the ordering, the type from F is at least as specialized as the type from G, and,
  • when performing deduction using the transformed F as the argument template and G as the parameter template, after deduction is done for all pairs of types, all template parameters used in the types from G that are used to determine the ordering have values, and those values are consistent across all pairs of types.

F is more specialized than G if F is at least as specialized as G and G is not at least as specialized as F.

将整个当前 [p11] 设为注释。

(1391决议给[14.8.2.5p4]添加的注释也需要调整-[14.8.2.1]没问题,[14.8.2.4]不行。)


对于 MSVC,在某些情况下,P 中的所有模板参数似乎都需要在推导期间接收值 对于特定的 P / A 以便从 A 推导成功到 P。我认为这可能是导致您的示例和其他示例中出现实施分歧的原因,但我至少看到过一个上述情况似乎不适用的情况,所以我不确定该相信什么。

上面的语句似乎适用的另一个示例:在您的示例中将 template<typename T> void bar(T, T) 更改为 template<typename T, typename U> void bar(T, U) 交换结果:调用在 Clang 和 GCC 中不明确,但解析为 b 在 MSVC 中。

一个没有的例子:

#include <iostream>

template<class T> struct A { using a = T; };
template<class, class> struct B { };

template<class T, class U> void f(B<U, T>) { std::cout << "#1\n"; }
template<class T, class U> void f(B<U, typename A<T>::a>) { std::cout << "#2\n"; }

int main()
{
   f<int>(B<int, int>());
}

正如预期的那样,这在 Clang 和 GCC 中选择了 #2,但 MSVC 拒绝调用不明确;不知道为什么。


标准中描述的偏序算法谈到合成 唯一类型、值或 class 模板 以生成参数。 Clang 通过......不合成任何东西来管理它。它只使用依赖类型的原始形式(如声明的那样)并以两种方式匹配它们。这是有道理的,因为替换合成类型不会添加任何新信息。它不能更改 A 类型的形式,因为通常没有办法判断被替换的形式可以解析为哪些具体类型。合成类型未知,这使得它们与模板参数非常相似。

当遇到作为非推导上下文的 P 时,Clang 的模板参数推导算法会简单地跳过它,为该特定步骤返回 "success"。这不仅发生在部分排序期间,而且发生在所有类型的推导中,不仅发生在函数参数列表的顶层,而且每当遇到复合类型形式的非推导上下文时递归发生。出于某种原因,我第一次看到它时感到很惊讶。想想当然是有道理的,而且是按照标准的([...]不参与类型推导[...] in [14.8. 2.5p4]).

这与 Richard Corden's comments to 一致,但我必须实际查看编译器代码才能理解所有含义(不是他的回答的错误,而是我自己的错误 - 程序员在代码中思考等等) ).

我在 this answer.

中包含了更多关于 Clang 实现的信息