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

时间:2014-12-19 21:10:46

标签: c++ templates c++11 ambiguous

考虑这段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");
}

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

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

来自#1

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

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

  • 代表#1 数组到指针转换,类别左值转换,根据[13.3.3.1.1];根据{{​​1}}比较转化序列时,系统会忽略此转化类别,因此基本上与身份转换相同;
  • 对于#2 ,参数属于引用类型并直接绑定到参数,因此,根据[13.3.3.2],这又是身份转换

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

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

  • 使用#1 作为参数并使用#2 作为参数进行模板参数推导:我们发明了一个值[14.8.2.4]来替换M,{ {1}}推导为NT作为参数可以从int类型的参数初始化,一切都很好。据我所知,#2 至少与所有相关类型的#1 一样专业。
  • 使用#2 作为参数并使用#1 作为参数进行模板参数推导:我们发明了一种类型const char*来代替char[M],无法从类型U的参数(无关类型)初始化类型T的参数,无法从类型int的参数初始化类型U的参数,并且无法从参数中推导出非类型参数char[N]的值,因此......一切都失败了。据我所知,#1 至少与所有涉及的类型#2 一样专业。

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

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

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

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

char[N]

将此调用拒绝为含糊不清的错误消息。 Clang的错误是:

array of const

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

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

2 个答案:

答案 0 :(得分:9)

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

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

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

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

清除泥土?也许有几个例子会有所帮助。

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

#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);
}

并打印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();
}

来自Clang的错误

error: implicit instantiation of undefined template 'A<int [3]>'

无法使用特化,因为T*int[3]在这种情况下不匹配,因此编译器会尝试实例化主模板。

这是在部分订购时使用的第二种扣除。


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

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

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

  • 使用#1 作为参数并使用#2 作为参数进行模板参数推导:我们发明了一个值M来替换N,{ {1}}被推断为T,但int 类型的参数不匹配const char*类型的参数,因此#2 < / em> 至少与第二对类型#1 一样专门。
  • 使用#2 作为参数并将#1 作为参数进行模板参数推导:我们创建了一个char[M]类型来代替U,{ {1}}和T不匹配(不同类型),类型int的参数与类型U的参数不匹配,以及非类型模板参数的值char[N]无法从参数中推断出来,因此#1 至少与#2 一样专门用于任何一种类型

因为,为了被选择,模板需要至少与所有类型的模板一样专用,因此 这两个模板都不比其他更专业,并且调用不明确


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

其中的例子是:

const char*

作者认为,直观地说,第二个模板应该被选择为更加专业化,并且目前还没有发生(模板比其他模板更专业)。

然后他解释说原因是从N移除了template<class C> void foo(const C* val) {} template<int N> void foo(const char (&t)[N]) {} 限定符,产生const,这会导致扣除失败并将const char[N]作为参数。

但是,根据我目前的理解,在这种情况下,扣除将失败,char[N]或无const C*。 Clang和GCC中的当前实现证实了这一点:如果我们从两个函数模板的参数中删除const限定符并使用const参数调用const,则调用仍然不明确。在部分排序期间,数组和指针根据当前规则根本不匹配。

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


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

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

最后一条评论是:

  

有一些观点认为有这种情况是可取的   订购,但我们不认为值得花时间去研究它   现在。如果我们在某个时刻看一些较大的偏序变化,   我们会再考虑一下。

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

答案 1 :(得分:0)

最初,我认为您的代码问题是您没有考虑功能类型调整。函数类型调整会导致带有边界的数组被解释为指向该类型的指针。 我试图通过询问编译器通过模板静态看到的内容来找到问题的解决方案,但我获得了更有趣的结果:

#include <iostream>
#include <type_traits>

template<typename T, std::size_t N>
void is_same( const T* _left, const char(&_right)[N] )
{
 typedef decltype(_left) LeftT;
 typedef decltype(_right) RightT;

 std::cout << std::is_same<LeftT,const char*>::value << std::endl;
 std::cout << std::is_same<LeftT,const char(&)[3]>::value << std::endl;
 std::cout << std::is_same<LeftT,const char(&)[4]>::value << std::endl;
 std::cout << std::is_same<RightT,const char*>::value << std::endl;
 std::cout << std::is_same<RightT,const char(&)[3]>::value << std::endl;
 std::cout << std::is_same<RightT,const char(&)[4]>::value << std::endl;
}

int main()
{
 std::cout << std::boolalpha;

 is_same( "ab", "cd" );

 return 0;
}

产量收益率: 真正 假 假 假 真正 假

在这种情况下,编译器能够区分参数。

编辑1: 这是一些更多的代码。引入rvalue引用使函数更具区别性。

#include <iostream>

// f
template<typename _T>
 void f( _T, const char* )
 {
  std::cout << "f( _T, const char* )" << std::endl;
 }

template<std::size_t _kN>
 void f( int, const char(&)[_kN] )
 {
  std::cout << "f( int, const char (&)[_kN] )" << std::endl;
 }

// g
template<typename _T>
 void g( _T, const char* )
 {
  std::cout << "g( _T, const char* )" << std::endl;
 }

template<std::size_t _kN>
 void g( int, const char(&&)[_kN] )
 {
  std::cout << "g( int, const char (&&)[_kN] )" << std::endl;
 }

// h
template<std::size_t _kN>
 void h( int, const char(&)[_kN] )
 {
  std::cout << "h( int, const char(&)[_kN] )" << std::endl;
 }

template<std::size_t _kN>
 void h( int, const char(&&)[_kN] )
 {
  std::cout << "h( int, const char (&&)[_kN] )" << std::endl;
 }

int main()
{
 //f( 7, "ab" ); // Error!
 //f( 7, std::move("ab") ); // Error!
 f( 7, static_cast<const char*>("ab") ); // OK
 //f( 7, static_cast<const char(&)[3]>("ab") ); // Error!
 //f( 7, static_cast<const char(&&)[3]>("ab") ); // Error!

 g( 7, "ab" ); // OK
 //g( 7, std::move("ab") ); // Error!
 g( 7, static_cast<const char*>("ab") ); // OK
 g( 7, static_cast<const char(&)[3]>("ab") ); // OK
 //g( 7, static_cast<const char (&&)[3]>("ab") ); // Error!

 h( 7, "ab" ); // OK (What? Why is this an lvalue?)
 h( 7, std::move("ab") ); // OK
 //h( 7, static_cast<const char*>("ab") ); // Error
 h( 7, static_cast<const char(&)[3]>("ab") ); // OK
 h( 7, static_cast<const char(&&)[3]>("ab") ); // OK

 return 0;
}