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

Bar*_*rry 20 c++ templates partial-ordering language-lawyer overload-resolution

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

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

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

根据Richard Corden的回答,对于"b"的推论,该表达式typename identity<UniqueB>::type被视为一种类型而不进行评估.也就是说,这将被合成,就像它是:

+---+-------------------------------+--------------------+
|   | Parameters                    | Arguments          |
+---+-------------------------------+--------------------+
| a | T, typename identity<T>::type | UniqueA, UniqueA   |
| b | T, T                          | UniqueB, UniqueB_2 |
+---+-------------------------------+--------------------+
Run Code Online (Sandbox Code Playgroud)

很明显,"b"的扣除失败了.这是两种不同的类型,所以你不能推导出T它们.

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

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

bog*_*dan 22

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

为了使事情变得更有趣,MSVC(我测试了12和14)拒绝这个调用是模棱两可的.我认为标准中没有任何东西能够最终证明哪个编译器是正确的,但我想我可能已经知道差异来自何处; 下面有关于此的说明.

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


第一,拟议的第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());
    }
    
    Run Code Online (Sandbox Code Playgroud)

    (#2第二个参数不用于部分排序,因此扣除成功#1,#2但不是相反).目前,这个电话是模棱两可的,应该可以说是这样.


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

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

对于P/ A对:

  • 如果P是非依赖性的,则当且仅当PA相同类型时,扣除才被视为成功.
  • 将推断的模板参数替换为出现的未推导的上下文P不会被执行,并且不会影响推论过程的结果.
  • 如果P除了仅出现在非推导上下文中的模板参数之外的所有模板参数成功推导出模板参数值,则扣除被认为是成功的(即使某些参数在P该特定的扣除过程结束时保持没有值)P/ Apair).

笔记:

  • 关于第二个项目符号点:14.8.2.5p1]关于寻找模板参数值的会谈,这将使P推导值代后,(称之为推断A),兼容A.这可能会导致对部分订购期间实际发生的事情产生混淆; 没有替代品.
  • 在某些情况下,MSVC似乎没有实现第三个要点.有关详细信息,请参阅下一节.
  • 第二和第三个要点还旨在涵盖P具有类似形式的案例A<T, typename U::b>,这些案例不在第1391期的措辞中.

将当前[p10]更改为:

函数模板F至少与专业的功能模板, G当且仅当:

  • 对于用于确定排序的每对类型,类型from F至少与来自的类型一样专门G,并且,
  • 在使用转换F作为参数模板和G作为参数模板执行推导时,在对所有类型对进行推导之后G,用于确定排序的类型中使用的所有模板参数 都具有值,并且这些值在所有类型的对.

F 更专业的G,如果F是至少作为专业GG不至少作为专业F.

使整个当前[p11]成为一个音符.

(由1391至[14.8.2.5p4]的决议增加的说明也需要调整 - 对于[14.8.2.1]是好的,但对[14.8.2.4]则没有.)


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

#2按照预期在Clang和GCC中选择,但是MSVC拒绝该呼叫是不明确的; 不明白为什么.


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

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

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

我在这个答案中包含了一些关于Clang实现的更多信息.


Ric*_*den 5

我认为关键在于以下声明:

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

类型扣除不执行"转换"检查.这些检查使用真实显式和推导的参数作为重载解析的一部分进行.

这是我对选择要调用的函数模板所采取的步骤的总结(所有引用都取自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. 如果两个函数特化的转换序列不是"更好",则使用部分排序算法来找到更专用的函数模板.(13.3.3)
  6. 部分排序算法仅检查类型推导是否成功.(14.5.6.2/2)

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