函数模板的部分排序 - 模糊调用

bog*_*dan 46 c++ templates ambiguous c++11

考虑一下这段C++ 11代码:

#include <iostream>
#include <cstddef>

template<typename T> void f(T, const char*) //#1
{ 
    std::cout << "f(T, const char*)\n"; 
}

template<std::size_t N> void f(int, const char(&)[N]) //#2
{ 
    std::cout << "f(int, const char (&)[N])\n"; 
}

int main()
{
    f(7, "ab");
}
Run Code Online (Sandbox Code Playgroud)

好吧,那么...选择哪个超载?在使用编译器输出溢出bean之前,让我们试着解释一下.

(所有对部分的引用都是针对C++ 11,ISO/IEC 14882:2011的最终标准文档.)

T#1推断出int,N#2推断出来3,两个专业都是候选者,两者都是可行的,到目前为止都很好.哪一个最好?

首先,考虑将函数参数与函数参数匹配所需的隐式转换.对于第一个参数,在任何一种情况下(身份转换)都不需要转换,int无处不在,所以这两个函数同样好.对于第二个,参数类型是const char[3],并且两个转换是:

  • 对于#1,数组到指针的转换,类别左值转换,根据[13.3.3.1.1]; 在根据转换序列进行比较时会忽略此转换类别[13.3.3.2],因此这与此目的的身份转换基本相同;
  • 对于#2,参数是引用类型并直接绑定到参数,因此,根据[13.3.3.1.4],这又是身份转换.

再一次,没有运气:两个功能仍然同样好.两者都是模板特化,我们现在必须看看哪个功能模板(如果有的话)更专业([14.5.6.2][14.8.2.4]).

编辑3:下面的描述很接近,但不太准确.请参阅我的回答,我认为这是对过程的正确描述.

  • 模板参数推导使用#1作为参数,#2作为参数:我们发明一个值M来替换N,T推导为int,const char*因为参数可以从类型的参数初始化char[M],一切都很好.据我所知,对于所涉及的所有类型,#2至少与#1一样专业.
  • 模板参数推导以#2为参数,#1为参数:我们发明了一个U替换类型,一个类型T的参数int不能从类型的参数U(无关类型)char[N]初始化,类型的参数不能从参数中初始化类型const char*,并且N不能从参数中推导出非类型参数的值,所以......一切都失败了.据我所知,对于所有涉及的类型,#1至少不如#2那么专业.

编辑1:以上内容是根据Columbo和dyp的评论编辑的,以反映在这种情况下尝试模板参数推断之前删除引用的事实.

编辑2:根据来自hvd的信息,顶级cv限定符也被删除.在这种情况下,它意味着const char[N]变成char[N],因为数组元素上的cv-qualifiers也适用于数组本身(array of constconst array可以说是a ); 这在C++ 11标准中根本不明显,但已经为C++ 14澄清了.

基于以上所述,我会说功能模板的部分排序应该选择#2作为更专业的,并且调用应该解决它而没有歧义.

现在,回到残酷的现实.GCC 4.9.1和Clang 3.5.0都有以下选项

-Wall -Wextra -std=c++11 -pedantic 
Run Code Online (Sandbox Code Playgroud)

使用类似的错误消息拒绝该调用是不明确的.Clang的错误是:

prog.cc:16:2: error: call to 'f' is ambiguous
    f(7, "ab");
    ^
prog.cc:4:27: note: candidate function [with T = int] 
template<typename T> void f(T, const char*) //#1 
                         ^
prog.cc:9:30: note: candidate function [with N = 3] 
template<std::size_t N> void f(int, const char(&)[N]) //#2 
                            ^
Run Code Online (Sandbox Code Playgroud)

Visual C++ 2013的IntelliSense(据我所知,基于EDG编译器)也将调用标记为含糊不清.有趣的是,VC++编译器继续编译没有错误的代码,选择#2.(是的!它同意我的意见,所以一定是对的.)

对于专家来说,显而易见的问题是,为什么呼叫模糊不清?我错过了什么(在部分订购区域,我猜)?

bog*_*dan 9

我将我目前对该问题的理解细节作为答案发布.我不确定它是否会成为最后一个词,但它可以作为进一步讨论的基础,如果需要的话.来自dyp,hvd和Columbo的评论对于查找下面引用的各种信息非常重要.

我怀疑,问题在于功能模板的部分排序规则.部分[14.8.2.4](在部分排序期间推导模板参数)表示,在删除引用和cv限定符的初步转换之后,类型推导是按照[14.8.2.5](从类型中推导模板参数)中所述完成的.该部分与引用函数调用的部分不同 - 即[14.8.2.1](从函数调用中推导出模板参数).

当从函数参数类型推导出模板参数时,会有一些允许的特殊情况; 例如,T函数参数的函数参数中使用的模板参数T*可以推导出T[i],因为在这种情况下允许数组到指针的转换.然而,这不是在部分排序期间使用的演绎过程,即使我们仍在谈论功能.

我想在部分排序期间考虑模板参数推导规则的简单方法是说它们与匹配类模板特化时推导模板参数的规则相同.

像泥一样清楚?也许有几个例子会有所帮助.

这是有效的,因为它使用规则从函数调用中推导出模板参数:

#include <iostream>
#include <type_traits>

template<typename T> void f(T*)
{
    std::cout << std::is_same<T, int>::value << '\n';
}

int main()
{
    int a[3];
    f(a);
}
Run Code Online (Sandbox Code Playgroud)

和打印1.

这不,因为它使用规则从类型中推导出模板参数:

#include <iostream>

template<typename T> struct A;

template<typename T> struct A<T*>
{
    static void f() { std::cout << "specialization\n"; }
};

int main()
{
    A<int[3]>::f();
}
Run Code Online (Sandbox Code Playgroud)

并且来自Clang的错误是

error: implicit instantiation of undefined template 'A<int [3]>'
Run Code Online (Sandbox Code Playgroud)

专业化无法使用,因为T*int[3]没有在这种情况下匹配,所以编译器试图实例主模板.

这是在部分订购期间使用的第二种推论.


让我们回到我们的函数模板声明:

template<typename T> void f(T, const char*); //#1
template<std::size_t N> void f(int, const char(&)[N]); //#2
Run Code Online (Sandbox Code Playgroud)

我对部分排序过程的描述变为:

  • 模板参数推导与#1的参数和#2作为参数:我们发明的值M来代替N,T被推断为int,但类型的参数const char* 匹配类型的参数char[M],所以#2不是至少为专门为# 1表示第二对类型.
  • 模板参数推导与#2作为参数和#1作为参数:我们发明一种类型U来取代T,int并且U不匹配(不同类型),类型的参数char[N]不匹配类型的参数const char*,以及所述非的值类型模板参数N不能从参数推导出,所以#1至少专门的作为#2对任一对的类型.

因为,为了被选择,模板需要至少与所有类型的模板一样专用,因此,模板不是比另一模板更专业,并且调用是模糊的.


上面的解释与Core Language Active Issue 1610(由hvd提供的链接)中的类似问题的描述有些不同.

那里的例子是:

template<class C> void foo(const C* val) {}
template<int N> void foo(const char (&t)[N]) {}
Run Code Online (Sandbox Code Playgroud)

作者认为,直观地说,第二个模板应该被选择为更专业化,并且目前不会发生这种情况(模板都不比其他模板更专业).

然后他解释说,原因是constconst char[N]屈服中移除限定符char[N],这导致演绎失败并const C*作为参数.

但是,根据我目前的理解,扣除在这种情况下会失败,const或者没有const.这是由锵和GCC当前实现证实:如果去掉const从两个函数模板参数限定词,并呼吁foo()char[3]参数,调用仍然是不明确的.在部分排序期间,根据当前规则,数组和指针根本不匹配.

话虽如此,我不是委员会的成员,所以可能还有比我目前理解的更多.


更新:我最近偶然发现了另一个可追溯到2003年的核心活动问题:问题402.

那里的例子相当于1610年的例子.关于该问题的评论清楚地表明,根据部分排序算法,两个重载是无序的,正是因为在部分排序期间缺少数组到指针衰减规则.

最后的评论是:

有一些观点认为这个案例是有序的,但我们认为现在不值得花时间去处理它.如果我们在某个时刻查看一些较大的偏序变化,我们将再次考虑这一点.

因此,我非常有信心上面给出的解释是正确的.