5

最近,我发现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抱怨候选函数不明确。让我们看看标准对部分排序的看法。
temp.deduct.partial#2

推演过程使用转换后的类型作为参数模板,将另一个模板的原始类型作为参数模板。对于偏序比较中涉及的每种类型,此过程执行两次:一次使用转换后的模板 1 作为参数模板,模板 2 作为参数模板,再次使用转换后的模板 2 作为参数模板和模板 1作为参数模板。

#1因此,我们可以分别得到候选和的两组 P/A 对#2。一个是变换#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|  
|----------------------------------|-------|

对于非演绎的上下文,它的值可以从别处获得。

然而,在某些情况下,该值不参与类型推导,而是使用在其他地方推导或明确指定的模板参数的值。如果模板参数仅在非推导上下文中使用且未明确指定,则模板参数推导失败。

因此,我们可以忽略对typename unknow_context<U>::type/ int,并考虑U/ UniqueAU可以推导出来UniqueA

如果给定类型的推导成功,则参数模板中的类型被认为至少与参数模板中的类型一样特化。

#b中,我们得到#2至少与 一样专业的结果#1

问题在#a set. 第二对没有问题,可以说第二部分#1至少和第二部分一样专业#2

但是,函数模板 F 是否比函数模板 G 更专业的规则定义为:

函数模板 F 至少与函数模板 G 一样特化,如果对于用于确定排序的每一对类型,来自 F 的类型至少与来自 G 的类型一样特化。如果 F 位于至少和 G 一样专业,而 G 至少不像 F 那样专业

因此,我们注意 pair int / typename unknow_context<UniqueA>::type,尽管规则说“P 不用于确定排序”。但是,标准中的重要说明说:

[ 注意:在 [temp.deduct.call] 和 [temp.deduct.partial] 下,如果 P 不包含出现在推导上下文中的模板参数,则不进行推导,因此P 和 A 不必具有相同的形式。——尾注]

所以,就目前而言,从P/A set #a,#1起码还是和#2(演绎成功)一样专业。所以,我认为最新的GCC应该是正确的(模棱两可,两者都不比另一个更专业)。

问题一:

哪个编译器是正确的?

问题2:

是否在部分排序期间执行特化的实例化?该标准似乎没有指定是否将执行实例化。

#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 pair特殊之处,即:

|----|------------------------------------------------------------|
|P   |A                                                           |    
|----|------------------------------------------------------------|  
|T   |typename unknow_context<UniqueA>::type /*Is it equivalent to| 
|    | UniqueA? */                                                |  
|----|------------------------------------------------------------|   
|T   |UniqueA                                                     |  
|----|------------------------------------------------------------|  

是否typename unknow_context<UniqueA>::type会计算到UniqueA?似乎所有编译器都将typename unknow_context<UniqueA>::type其视为唯一类型,而不是将其计算为UniqueA.

4

1 回答 1

2

(由于这个答案与 OP 一致并且不同意 Clang 和 GCC(主干)的实现,它可能是一个有点不完整的答案,但至少它突出了部分排序规则的一些现有问题,特别是对于部分排序涉及非推断上下文)


问题1:哪个编译器是正确的?

让我们首先注意,从 GCC 11(/trunk) 开始,两个编译器都同意他们的解释,并选择候选 #2 比候选 #1 更专业,并且根据[over.match.best]/1.7重载决议选择前者作为最佳可行功能。

但是,您对[temp.func.order]的论点似乎是有效的,尤其是对[temp.deduct.partial]/4的强调:

[...]如果一个特定P的不包含参与模板参数推导的模板参数,则不P用于确定排序。

意思是

[...]用于确定排序的每对类型[...]

[temp.deduct.partial]/10中不应该考虑排序的(P, A)(int, typename unknow_context<UniqueA>::type),并且对于剩余的对候选者#1 至少与候选者#2 一样专业,这意味着候选者#1 至少被指定为候选者#2 为每 [temp.deduct.partial]/10。

因此,我认为 Clang 是错误的,并且根据 GCC 11(/trunk),GCC 再次是错误的,但正如我在下面强调的那样,在涉及非推断上下文的边缘情况下,部分排序规则在历史上一直是,未指定(02-0051/N1393解决了其中的许多问题),而如今,至少是模糊的(可能仍然未指定),因为我们看到它们的许多实现差异。


问题2:是否在偏序时进行特化的实例化?

我不确定最相关的部分;它可能属于[temp.inst]/9

除非需要这种实例化,否则实现不应隐式实例化 [...] 。

以及[temp.deduct]/8的非规范性注释:

[注意:对类型和表达式的替换可能会导致类模板特化和/或函数模板特化的实例化,[...] end note ]

但是的,作为部分排序的一部分,作为模板参数替换推导参数的一部分,需要合理实例化 of 的特化。如果编译器的情况如此,并且 GCC 和 Clang 都同意,拒绝以下程序,我们可以使用注入的朋友技巧来强制可诊断的 ODR 违规:unknown_context

// 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. 部分排序和非演绎论证

目前尚不清楚重载和偏序如何处理非推导的相应参数对。

[...]

John Spicer:关于在偏序规则中是否正确处理非推导上下文可能存在(也可能不存在)问题

并注意编译器,尤其是 Clang 和 GCC,在如何在涉及非推导上下文的边缘情况下应用偏序规则方面始终存在分歧。

来自 GCC 的 Jason Merrill 编写了CWG 问题 1337,它被标记为 CWG 问题 455 的副本。Jason 积极参与许多开放的 GCC 错误报告,特别指出

  • 错误 86193 -具有依赖类型的非类型模板参数的部分排序

[强调我的]

杰森美林 2018-06-18 19:09:13 UTC

同样,G++(和 EDG)拒绝,而 clang 接受。我认为 G++ 就在这里:[...]

这似乎是标准中未指定的领域。

以及

  • 错误 91133 - [8/9/10/11 回归] 错误的“部分专业化不比专业化”错误

这实际上是一个偏序问题;[...]

这被 GCC 拒绝为模棱两可,至少可以追溯到 4.1。EDG/icc 也拒绝了它。它被 clang 和 msvc 接受,就像原始测试用例一样。

问题在于从 #2 中对 #1 的偏序推导:我们从第二个参数推导出 U 的 int ,从第三个参数推导出 U 的 Id::type ,而那些不同意,所以推导第三个参数双向失败,功能不明确。

这与开放核心问题 455 和 1337 有关。

我不知道 clang/msvc 使用什么理由来得出 #2 更专业的结论。

因此,根据上面的引述,他们对(可能未指定的)标准的历史不同解释似乎是有意的。这种实现差异通常是标准相关部分模糊的标志,充其量是(未指定,最坏的情况)。

另请参阅 GCC错误报告 67228


来自 GCC 的歧义错误只是回归吗?

旧版本的 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);  
}

如下:

  • 直到并包括 GCC 7.3.0:打印#2(与 clang 相同)
  • GCC 8/9/10:错误:不明确的调用
  • GCC 主干/11:返回打印#2

我还没有找到关于 [8/9/10] 回归的错误报告,但似乎 GCC 现在重新使用与 Clang 相同的解释(接受程序并发现 #2 更专业),这两者我们中的一些人似乎同意这是错误的(候选人#1 应该被认为至少与候选人#2 一样专业)。

于 2020-12-09T13:42:17.330 回答