暂时切换到极其迂腐的模式,是的,我认为你在标准中遗漏了一些东西,不,在这种情况下它不应该有任何区别。
所有标准参考均参考当前工作草案 N4527。
[14.5.5.2p1] 说:
对于两个类模板部分特化,如果根据函数模板的排序规则 (14.5.6.2) 对两个函数模板进行以下重写,则第一个函数模板比第二个函数模板更特化:
- 第一个函数模板具有与第一个部分特化相同的模板参数,并且具有单个函数参数,其类型是具有第一个部分特化的模板参数的类模板特化,并且
- 第二个函数模板具有与第二个偏特化相同的模板参数,并且具有单个函数参数,其类型是具有第二个偏特化的模板参数的类模板特化。
前往 [14.5.6.2p1]:
[...]在以下上下文中使用重载函数模板声明的部分顺序来选择函数模板特化所引用的函数模板:
- 在调用函数模板特化 (13.3.3) 的重载决议期间;
- 当一个函数模板特化的地址被取走时;
- when a placement operator delete that is a function template specialization is selected to match a placement operator new (3.7.4.2, 5.3.4);
- 当友元函数声明 (14.5.4)、显式实例化 (14.7.2) 或显式特化 (14.7.3) 指的是函数模板特化。
没有提到类模板特化的部分排序。然而,[14.8.2.4p3] 说:
用于确定排序的类型取决于完成部分排序的上下文:
- 在函数调用的上下文中,使用的类型是函数调用具有参数的那些函数参数类型。
- 在调用转换函数的上下文中,使用转换函数模板的返回类型。
- 在其他上下文 (14.5.6.2) 中,使用函数模板的函数类型。
即使它回溯到 [14.5.6.2],它确实说的是“其他上下文”。我只能得出结论,当将偏序算法应用于根据 [14.5.5.2] 中的规则生成的函数模板时,使用的是函数模板的函数类型,而不是参数类型列表,因为它会发生在函数中称呼。
因此,在您的第一个片段中选择部分特化t
不等同于涉及函数调用的情况,而是等同于采用函数模板地址的情况(例如),该模板也属于“其他上下文”:
#include <iostream>
template<typename> struct s { typedef void v, w; };
template<typename, typename = void> struct t { };
template<typename C> void f(t<C, typename C::v>) { std::cout << "t<C, C::v>\n"; }
template<typename C> void f(t<s<C>, typename s<C>::w>) { std::cout << "t<s<C>, s<C>::w>\n"; }
int main()
{
using pft = void (*)(t<s<int>>);
pft p = f;
p(t<s<int>>());
}
(由于我们仍处于极度迂腐模式,我重写了函数模板,就像 [14.5.5.2p2] 中的示例一样。)
不用说,这也编译和打印t<s<C>, s<C>::w>
。它产生不同行为的机会很小,但我必须尝试一下。考虑到算法是如何工作的,如果函数参数是引用类型(在函数调用的情况下触发 [14.8.2.4] 中的特殊规则,但在其他情况下不触发),将会有所不同,但是这样的形式不能出现在类模板特化生成的函数模板中。
所以,这整个弯路对我们没有一点帮助,但是......这是一个language-lawyer
问题,我们必须在这里有一些标准的报价......
有一些与您的示例相关的活跃核心问题:
1157包含我认为相关的注释:
模板参数推导是尝试匹配 aP
和 a deduced
A
;P
但是,如果和推导A
不兼容,则不指定模板参数推导失败。这可能发生在存在非推断上下文的情况下。尽管 14.8.2.4 [temp.deduct.partial] 第 9 段中有括号声明,但模板参数推导可能成功地确定每个模板参数的模板参数,同时产生A
与相应的不兼容的推导P
。
我不完全确定是否如此明确地指定。毕竟,[14.8.2.5p1] 说
[...] 找到模板参数值 [...],使 P 在替换推导值 [...] 后与 A 兼容。
[14.8.2.4] 全文引用 [14.8.2.5]。然而,很明显,当涉及非推导上下文时,函数模板的部分排序不会寻找兼容性,并且改变它会破坏很多有效的情况,所以我认为这只是标准中缺乏适当的规范.
在较小程度上,1847与出现在模板特化的参数中的非推导上下文有关。它引用了1391的决议;我认为该措辞存在一些问题-此答案中有更多详细信息。
对我来说,所有这些都表明您的示例应该有效。
和你一样,我对三个不同的编译器中存在相同的不一致这一事实非常感兴趣。在我验证 MSVC 14 表现出与其他版本完全相同的行为后,我更加感兴趣。所以,当我有时间的时候,我想我会快速看看 Clang 做了什么;事实证明它并不快,但它产生了一些答案。
所有与我们案例相关的代码都在lib/Sema/SemaTemplateDeduction.cpp
.
推演算法的核心是DeduceTemplateArgumentsByTypeMatch
函数;所有演绎的变体最终都会调用它,然后递归地使用它来遍历复合类型的结构,有时借助重载DeduceTemplateArguments
的函数集和一些标志来根据特定的演绎类型调整算法完成并查看类型表单的各个部分。
关于这个函数需要注意的一个重要方面是它严格地处理扣除,而不是替换。它比较类型形式,为出现在推导上下文中的模板参数推导模板参数值,并跳过非推导上下文。它所做的唯一其他检查是验证模板参数的推导参数值是否一致。我在上面提到的答案中写了更多关于 Clang 在部分排序期间进行扣除的方式。
对于函数模板的偏序,算法从Sema::getMoreSpecializedTemplate
成员函数开始,它使用类型标志enum TPOC
来确定正在执行偏序的上下文;枚举数是TPOC_Call
, TPOC_Conversion
, 和TPOC_Other
; 不言自明。isAtLeastAsSpecializedAs
然后,此函数在两个模板之间来回调用两次,并比较结果。
isAtLeastAsSpecializedAs
打开标志的值,在此TPOC
基础上进行一些调整,最后直接或间接调用DeduceTemplateArgumentsByTypeMatch
. 如果返回Sema::TDK_Success
,isAtLeastAsSpecializedAs
则只进行一次检查,以验证用于部分排序的所有模板参数是否具有值。如果这也很好,它会返回true
。
这就是函数模板的部分排序。根据上一节中引用的段落,我期望类模板专业化的部分排序可以Sema::getMoreSpecializedTemplate
使用适当构造的函数模板和 的标志来调用TPOC_Other
,并且一切都会从那里自然流动。如果是这种情况,您的示例应该有效。惊喜:事实并非如此。
类模板特化的部分排序从Sema::getMoreSpecializedPartialSpecialization
. 作为一种优化(危险信号!),它不合成函数模板,而是DeduceTemplateArgumentsByTypeMatch
直接在类模板特化本身上进行类型推导,作为P
and的类型A
。这可以; 毕竟,这就是函数模板的算法最终会做的事情。
但是,如果在推导过程中一切顺利,它就会调用FinishTemplateArgumentDeduction
(类模板特化的重载),它会进行替换和其他检查,包括检查特化的替换参数是否与原始参数等价。如果代码正在检查部分特化是否与一组参数匹配,这会很好,但在部分排序期间并不好,并且据我所知,这会导致您的示例出现问题。
所以,看起来理查德科登关于发生的事情的假设是正确的,但我不完全确定这是故意的。这对我来说更像是一个疏忽。我们如何最终使所有编译器都以相同的方式运行仍然是一个谜。
FinishTemplateArgumentDeduction
在我看来,删除对from的两个调用Sema::getMoreSpecializedPartialSpecialization
不会有任何害处,并且会恢复部分排序算法的一致性。也不需要额外检查(由 完成isAtLeastAsSpecializedAs
)所有模板参数都有值,因为我们知道所有模板参数都可以从特化的参数中推导出来;如果不是,则偏特化将无法匹配,因此我们一开始就不会进行偏序。(首先是否允许这种部分专业化是问题 549的主题。Clang 会针对这种情况发出警告,MSVC 和 GCC 会发出错误。无论如何,这不是问题。)
作为旁注,我认为所有这些都适用于变量模板特化的重载。
不幸的是,我没有为 Clang 设置构建环境,因此我目前无法测试此更改。