最近,我发现GCC
改变了部分排序时的行为,具体情况如下:
#include <iostream>
template<class T>
struct unknow_context{
using type = int;
};
template<class U>
void show(typename unknow_context<U>::type, U){ // candidate #1
std::cout<<"#1\n";
}
template<class T>
void show(int, T){ // candidate #2
std::cout<<"#2\n";
}
int main(){
show(0,0);
}
结果是,Clang打印#2
(任何版本的Clang
都打印了一致的结果)。但是,GCC
有不同的行为。旧版本的GCC打印#2
,相反,最新的GCC抱怨候选函数是模棱两可的。让我们看看标准对部分排序的规定。
temt. enpt.部分#2
推演过程使用转换后的类型作为参数模板,另一个模板的原始类型作为参数模板。这个过程对部分排序比较中涉及的每个类型做两次:一次使用转换后的模板-1作为参数模板,模板-2作为参数模板,再次使用转换后的模板-2作为参数模板,模板-1作为参数模板。
因此,我们可以分别为候选#1
和#2
得到两组P/A对。一个是转换后的#1
为A,原始#2
为P。另一个是原始#1
为P,转换后的#2
为A。所以这两个集合将如下给出:
#a
|--------|------------------------------------------|
| P (#2) | A (#1) |
|--------|------------------------------------------|
| int | typename unknow_context<UniqueA>::type |
|--------|------------------------------------------|
| T | UniqueB |
|--------|------------------------------------------|
T
可以从UniqueB
中推导出来。对于第一组,规则说:
如果特定的P不包含参与模板参数推导的模板参数,则该P不用于确定排序。
所以,我们先把它们放在一边,然后再考虑。
#b
|----------------------------------|-------|
|P (#1) |A (#2) |
|----------------------------------|-------|
| typename unknow_context<U>::type |int |
|----------------------------------|-------|
| U |UniqueA|
|----------------------------------|-------|
对于非推导上下文,其值可以从其他地方获得。
但是,在某些上下文中,该值不参与类型推导,而是使用在其他地方推导或显式指定的模板参数的值。如果模板参数仅在非推导上下文中使用并且未显式指定,则模板参数推导将失败。
因此,我们可以忽略对typenameunknow_context
如果给定类型的推导成功,则参数模板中的类型被认为至少与参数模板中的类型一样专业。
从#b
,我们得到#2
至少与#1
一样专业的结果。
问题在#a set
中。第二对没有问题,我们可以说#1
的第二部分至少和#2
的第二部分一样专业。
但是,函数模板F是否比函数模板G更专业的规则定义为:
如果对于用于确定排序的每一对类型,来自F的类型至少与来自G的类型一样专业,则函数模板F至少与函数模板G一样专业,如果F至少与G一样专业,并且G至少不像F一样专业,则F比G更专业。
因此,我们注意对int/typenameunknow_context
[注意:在[temp.deduct.call]和[temt.扣除.部分]下,如果P不包含出现在推导上下文中的模板参数,则不进行推导,因此P和A不需要具有相同的形式。
所以,到目前为止,从P/A集合#a
来看,#1
至少还是和#2
一样专业(推演成功)。所以,我认为最新的GCC
应该是正确的(模棱两可,两者都没有比另一个更专业)。
哪个编译器是正确的?
是否在部分排序时执行特化的实例化?标准似乎没有规定是否会执行实例化。
#include <iostream>
template<class T>
struct unknow_context{
using type = T;
};
template<class U>
void show(typename unknow_context<U>::type, U){
std::cout<<"#1\n";
}
template<class T>
void show(T, T){
std::cout<<"#2\n";
}
int main(){
show(0,0);
}
都选择了#2
。我担心其中的特定P/A对
,即:
|----|------------------------------------------------------------|
|P |A |
|----|------------------------------------------------------------|
|T |typename unknow_context<UniqueA>::type /*Is it equivalent to|
| | UniqueA? */ |
|----|------------------------------------------------------------|
|T |UniqueA |
|----|------------------------------------------------------------|
typename是否unknow_context
(由于这个答案同意OP,不同意Clang和GCC(主干)的实现,它可能是一个有点不完整的答案,但至少它突出了部分排序规则的一些现有问题,特别是对于涉及非推导上下文的部分排序)
问题1:哪个编译器是正确的?
让我们首先注意到,从GCC11(/主干)开始,两个编译器都同意他们的解释,并选择候选#2比候选#1更专业,并且根据[over.match.best]/1.7重载分辨率选择前者作为最佳可行函数。
但是,您的[temp. func.order]的论点似乎是有效的,尤其是对[temp.quot.部分]/4的强调:
[…]如果特定的P
不包含参与模板参数推导的模板参数,则该P
不用于确定排序。
意味着
[…]用于确定排序的每对类型[…]
在/10中不应该考虑(P, A)
对(int,typenameunknow_context
因此,我认为Clang是错误的,根据GCC11(/主干),GCC再次是错误的,但正如我在下面强调的那样,涉及非推导上下文的边缘情况下的部分排序规则在历史上一直被低估(02-0051/N1393解决了其中的许多问题),而现在,至少是模糊的(可能仍然被低估),因为我们看到了它们的许多实现差异。
问题2:是否在部分排序期间执行特化的实例化?
我不确定最相关的部分;它可能属于[temp. inst]/9,
实现不应隐式实例化[…],除非需要这种实例化。
和[temp. de和]/8的非规范性注释:
[注意:对类型和表达式的替换可能会导致类模板特殊化和/或函数模板特殊化的实例化,[…]结束说明]
但是,是的,合理地实例化unknown_context
的特殊化将需要作为模板参数的一部分替换推导参数作为部分排序的一部分。如果这对编译器成立,并且GCC和Clang都同意,拒绝以下程序,我们可以使用注入的朋友技巧来强制诊断ODR违规:
// Due to the injected friend, the identity class may
// only be instantiated once within a given TU, or
// the program, diagnosable ([basic.odr.def]/1).
template<class T>
struct identity {
using type = T;
friend void f() {}
};
void f();
template<class U>
void show(typename identity<U>::type, U) {}
template<class T>
void show(T, T) {}
int main(){
identity<char> i; // f() now defined
f(); // OK
show(0,0); // error: redefines f() as part of
// substitution in partial ordering.
}
带有指导性错误:
error: redefinition of 'f'
friend void f() {}
^
note: in instantiation of template class
'identity<int>' requested here
void show(typename identity<U>::type, U) {}
note: while substituting deduced template arguments
into function template 'show' [with U = int]
我们可以从活动/开放的CWG问题455开始:
455.部分排序和非推导参数
不清楚重载和部分排序如何处理相应参数的非推导对。
[...]
约翰·斯派塞:在部分排序规则中,可能存在(也可能没有)关于非推导上下文是否得到正确处理的问题
请注意,编译器,尤其是Clang和GCC,对于如何在涉及非推导上下文的边缘情况下应用部分排序规则一直存在分歧。
GCC的杰森·梅里尔写了CWG第1337期,被标记为CWG第455期的副本。杰森活跃在许多公开GCCbug报告中,特别注意到
那个[强调我的]
Jason Merrill 2018-06-18 19:09:13UTC
类似地,G(和EDG)拒绝,clang接受。我认为G就在这里:[…]
这似乎是标准中未明确说明的领域。
以及向
那
这实际上是一个部分排序的问题;[…]
这被GCC至少追溯到4.1时拒绝,也被EDG/icc拒绝。它被clang和msvc接受,就像原始测试用例一样。
问题在于#1从#2的部分排序推导:我们从第二个参数中推导出int for U,我从第三个参数中d::type for U,并且这些不一致,因此第三个参数的推导在两个方向上都失败了,并且函数是模糊的。
这与开放的核心问题455和1337有关。
我不知道clang/msvc使用什么理由来得出#2更专业的结论。
因此,根据上面的引文,他们对(可能未指明的)标准的历史不同解释似乎是有意的;这种实施差异通常是标准相关部分模糊的标志,充其量(未指明,最坏)。
另见GCCbug报告67228。
旧版本的GCC打印#2,相反,最新的GCC抱怨候选函数含糊不清。
如上所述,GCC: s对以下程序的行为
#include <iostream>
template<class T>
struct unknow_context{
using type = int;
};
template<class U>
void show(typename unknow_context<U>::type, U){ // candidate #1
std::cout<<"#1\n";
}
template<class T>
void show(int, T){ // candidate #2
std::cout<<"#2\n";
}
int main(){
show(0,0);
}
如下:
#2
(与clang相同)#2
我还没有找到一个bug的回归报告,但似乎GCC现在又回到了使用与Clang相同的解释(接受程序并发现#2更专业),我们俩似乎都同意这是错误的(候选人#1应该被认为至少与候选人#2一样专业)。