考虑以下简单(在模板问题的范围内)示例:
#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"的扣除失败了 . 这是两种不同的类型,所以你不能推导出它们两个.2449561_ .
但是,在我看来, A
的扣除应该失败 . 对于第一个参数,您将匹配 T == UniqueA
. 第二个参数是一个非推断的上下文 - 如果 UniqueA
可以转换为 identity<UniqueA>::type
,那么推论是否会成功?后者是替代失败,所以我不知道这种推论如何能够成功 .
在这种情况下,gcc和clang如何以及为什么更喜欢“a”重载?
2 回答
正如评论中所讨论的,我相信功能模板部分排序算法的几个方面在标准中根本不清楚或根本没有指定,这在您的示例中显示 .
为了使事情变得更有趣,MSVC(我测试了12和14)拒绝这个调用是模棱两可的 . 我认为标准中没有任何东西能够最终证明哪个编译器是正确的,但我想我可能已经知道差异来自何处;下面有关于此的说明 .
你的问题(和this one)要求我对事情的运作方式进行更多调查 . 我决定写这个答案不是因为我认为它是权威的,而是组织我在一个地方找到的信息(它不适合评论) . 我希望它会有用 .
首先,提议issue 1391的决议 . 我们在评论和聊天中广泛讨论了它 . 我认为,虽然它确实提供了一些澄清,但它也引入了一些问题 . 它将[14.8.2.4p4]更改为(粗体的新文本):
我认为这不是一个好主意,原因有以下几点:
如果
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
更专业:(
#2
的第二个参数不用于部分排序,因此扣除成功从#1
到#2
,但不是相反) . 目前,这个电话是模棱两可的,应该可以说是这样 .在查看了Clang对部分排序算法的实现之后,我认为可以改变标准文本以反映实际发生的情况 .
保持[p4]不变,并在[p8]和[p9]之间添加以下内容:
笔记:
关于第二个要点:[14.8.2.5p1]讨论在替换推导出的值(称之为推导出的
A
)后,找到将产生P
的模板参数值,与A
兼容 . 这可能会导致对部分订购期间实际发生的事情产生混淆;没有替代品 .在某些情况下,MSVC似乎没有实现第三个要点 . 有关详细信息,请参阅下一节 .
第二和第三个要点也旨在涵盖
P
具有A<T, typename U::b>
等形式的情况,这些情况不在问题1391中的措辞中 .将当前[p10]更改为:
使整个当前[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中的调用不明确,但在MSVC中解析为b
.其中一个例子没有:
这会按预期在Clang和GCC中选择
#2
,但MSVC拒绝该调用是不明确的;不明白为什么 .标准中描述的部分排序算法涉及合成唯一类型,值或类模板以生成参数 . Clang管理着......没有合成任何东西 . 它只使用依赖类型的原始形式(如声明的那样)并以两种方式匹配它们 . 这是有道理的,因为替换合成类型并不会改变
A
类型的形式,因为通常无法分辨替换形式可以解析的具体类型 . 合成类型是未知的,这使它们非常类似于模板参数 .当遇到
P
是一个非推导的上下文时,Clang的模板参数推导算法只是跳过它,通过返回特定步骤的"success" . 这不仅发生在部分排序期间,而且发生在所有类型的推论中,而不仅仅发生在函数参数列表的顶层,而是以复合类型的形式遇到非推断的上下文时递归发生 . 出于某种原因,我第一次看到它时发现这令人惊讶 . 考虑到它,它确实有意义,并且符合标准([...]不参与[14.8.2.5p4]中的类型推导[...]) .这与his answer的Richard Corden's注释是一致的,但我必须实际看到编译器代码才能理解所有含义(不是他的答案的错误,而是我自己的 - 程序员在代码中思考所有这些) .
我在this answer中执行了've included some more information about Clang' .
我认为关键在于以下声明:
类型扣除不执行“转换”检查 . 这些检查使用真实显式和推导的参数作为重载解析的一部分进行 .
这是我选择要调用的函数模板所采取的步骤的总结(所有引用都来自N3937,~C '14):
替换显式参数,并检查结果函数类型是否有效 . (14.8.2 / 2)
执行类型推导,并替换生成的推导参数 . 结果类型必须再次有效 . (14.8.2 / 5)
功能模板步骤1和2中的成功是专门的,并包含在重载决策的重载集中 . (14.8.3 / 1)
转换序列通过重载分辨率进行比较 . (13.3.3)
如果两个函数特化的转换序列不是'better',则使用部分排序算法来查找更专业的函数模板 . (13.3.3)
部分排序算法仅检查类型推导是否成功 . (14.5.6.2/2)
编译器已经在步骤4中知道在使用实参数时可以调用两个特化 . 步骤5和6用于确定哪些功能更专业 .