模板偏序 - 为什么部分演绎在这里成功

时间:2015-07-13 22:04:30

标签: c++ templates language-lawyer overload-resolution partial-ordering

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

#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 print&#34; a&#34;那里。根据[temp.deduct.partial]和[temp.func.order]中的规则,为了确定部分排序,我们需要合成一些独特的类型。所以我们有两次尝试扣除:

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

对于&#34; b&#34;的推论,根据Richard Corden's answer,表达式typename identity<UniqueB>::type被视为一种类型,不进行评估。也就是说,这将被合成为好像:

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

很明显,扣除&#34; b&#34;失败。这是两种不同的类型,因此您无法将T推断为两者。

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

gcc和clang如何以及为何更喜欢&#34; a&#34;在这种情况下超载?

2 个答案:

答案 0 :(得分:22)

正如评论中所讨论的,我认为功能模板部分排序算法的几个方面在标准中根本不清楚或根本没有指定,这在您的示例中显示。

为了让事情变得更有趣,MSVC(我测试过12和14)拒绝接听电话是不明确的。我不认为标准中的任何内容可以最终证明哪个编译器是正确的,但我想我可能已经知道差异来自何处;下面有关于此的说明。

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

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

  

从参数模板和上面提名的每种类型   参数模板中的相应类型用作类型   PA如果特定P不包含模板参数   参与模板参数推导,P不习惯   确定订购。

我认为这不是一个好主意,原因如下:

  • 如果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&#39}的第二个参数不用于部分排序,因此从#1#2的扣除成功,但不是相反)。目前,这个电话是模棱两可的,应该可以说是这样。

在查看了Clang部分排序算法的实现之后,我认为标准文本可以改变以反映实际发生的情况。

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

  

对于P / A对:

     
      
  • 如果P不依赖,则当且仅当PA属于同一类型时,才会认为扣除成功。
  •   
  • 不会执行将推导出的模板参数替换为P中出现的未推断的上下文,并且不会影响扣除过程的结果。
  •   
  • 如果成功推导出P的所有模板参数的模板参数值,只有那些仅出现在非推导上下文中的参数值,则扣除被认为是成功的(即使P中使用的某些参数仍然存在在特定P / A对的扣除流程结束时没有值。
  •   

注意:

  • 关于第二个要点:[14.8.2.5p1]讨论在替换推导出的值(称之为推导的P)之后找到将生成A的模板参数值 ,与A 兼容。这可能会导致对部分订购期间实际发生的事情产生混淆;没有替代品。
  • 在某些情况下,MSVC似乎没有实施第三个要点。有关详细信息,请参阅下一节。
  • 第二和第三个要点还旨在涵盖P形式为A<T, typename U::b>的情况,而问题1391中的措辞并未涵盖这些情况。

将当前[p10]更改为:

  

功能模板F至少与功能模板一样专业   G当且仅当:

     
      
  • 对于用于确定排序的每对类型,F中的类型至少与G中的类型一样专业,并且,
  •   
  • 使用转换后的F作为参数模板进行演绎,G作为参数模板,扣除完成后   对于所有类型对,从类型中使用的所有模板参数   用于确定排序的G具有值和那些值   值在所有类型对中都是一致的。
  •   
     如果F至少是专业的,那么

G F更专业   因为GG并不像F那样专业。

将整个当前[p11]作为一个音符。

(1391至[14.8.2.5p4]的决议增加的注释也需要调整 - [14.8.2.1]罚款,但[14.8.2.4]不适用。)< / p>

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

上述声明确实适用的另一个示例:在示例中将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拒绝该调用是不明确的;不明白为什么。

标准中描述的部分排序算法说明了合成唯一类型,值或类模板以生成参数。 Clang管理着......没有合成任何东西。它只使用依赖类型的原始形式(如声明的那样)并以两种方式匹配它们。这是有道理的,因为替换合成类型不会添加任何新信息。它无法改变A类型的形式,因为通常无法分辨替代形式可以解析的具体类型。合成的类型是未知的,这使它们非常类似于模板参数。

当遇到一个非推断的上下文的P时,Clang的模板参数推导算法只是跳过它,通过返回&#34; success&#34;对于那个特定的步骤。这不仅发生在部分排序期间,而且发生在所有类型的推论中,而不仅仅发生在函数参数列表的顶层,而是以复合类型的形式遇到非推断的上下文时递归发生。出于某种原因,我第一次看到它时发现这令人惊讶。考虑到它,它确实有意义,并且符合标准( [...]不参与[14.8.2.5p4]中的类型演绎[...] )。

这与对Richard Corden'shis answer注释一致,但我必须实际看到编译器代码才能理解所有含义(不是他的答案的错,而是我自己的错误 - 程序员的思考在代码和所有这些)。

我已在this answer中提供了有关Clang实施的更多信息。

答案 1 :(得分:5)

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

  

第二个参数是一个非推断的上下文 - 如果UniqueA可以转换为identity :: type,那么推理是否会成功?

类型扣除不会检查&#34;转换&#34;。这些检查使用真实的显式和推导的参数作为重载解析的一部分进行。

这是我对选择要调用的函数模板所采取的步骤的总结(所有引用均来自N3937,~C ++&#39; 14):

  1. 替换显式参数,并检查结果函数类型是否有效。 (14.8.2 / 2)
  2. 执行类型推导,并替换生成的推导参数。结果类型必须再次有效。 (14.8.2 / 5)
  3. 步骤1和2中成功的功能模板是专用的,包含在重载决策的重载集中。 (14.8.3 / 1)
  4. 通过重载分辨率比较转换序列。 (13.3.3)
  5. 如果两个功能专精的转换序列不是更好的&#39;部分排序算法用于查找更专业的函数模板。 (13.3.3)
  6. 部分排序算法仅检查类型推导是否成功。 (14.5.6.2/2)
  7. 编译器已经在步骤4中知道在使用实参数时可以调用两个特化。步骤5和6用于确定哪些功能更专业。