【问题标题】:Partial ordering of function templates - ambiguous call函数模板的部分排序 - 模糊调用
【发布时间】:2015-02-18 20:31:01
【问题描述】:

考虑这段 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。)

T from #1 推导出为intN from #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来代替NT推导为int, const char* 作为参数可以从char[M] 类型的参数初始化,一切都很好。据我所知,对于所有涉及的类型,#2 至少与 #1 一样专业。
  • #2为参数,#1为参数的模板参数推导:我们发明了一个U类型来代替T,一个@类型的参数987654341@不能从U类型的参数初始化(不相关的类型),char[N]类型的参数不能从const char*类型的参数初始化,非类型参数N的值不能从论点推导出来,所以......一切都失败了。据我所知,对于所有涉及的类型,#1 至少不像 #2 那样专门化。

编辑 1:以上内容已根据 Columbo 和 dyp 的 cmets 进行编辑,以反映在这种情况下尝试模板参数推导之前已删除引用的事实。

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

基于上述,我想说函数模板的部分排序应该选择 #2 作为更专业的,并且调用应该毫无歧义地解析它。

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

-Wall -Wextra -std=c++11 -pedantic 

以不明确的方式拒绝调用,并显示类似的错误消息。来自 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 
                            ^

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

对于专家来说显而易见的问题是,为什么调用不明确?我缺少什么(我猜是在偏序区域)?

【问题讨论】:

  • 我也不是,这是一个非常详细的问题,显示了努力。不过,无法帮助您解决问题。
  • @KerrekSB 不,我不这么认为。正如问题中所解释的,const char*const char[3] 在绑定到 const char[3] 参数时同样有效这一事实不足为奇。这就是为什么我们必须对函数模板进行部分排序来决定调用哪个重载。
  • 我实际上更希望将此类调用标记为模棱两可,因为调用错误的函数很容易出错。也许 VC++ 更符合标准的精神,而不是标准的文字。
  • 好吧,我想我们正在取得进展。基于 hvd 对其已删除答案的最后评论:[14.8.2.4] 表示,在初步转换之后,类型推导按照[14.8.2.5] 中的描述进行,这不是如何从函数调用中推导类型,而是从一个类型推导出模板参数。那里的规则不允许与函数参数进行相同的转换;我想它们可以被认为是用于从相应的参数推导类模板参数的相同规则。
  • @EvanCarslake 有时遗憾的是,否决票与内容无关,尤其是有问题时,因为您不必使用代表来否决问题。

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


【解决方案1】:

我将发布我当前对该问题的理解的详细信息作为答案。我不确定这将是最终决定,但如果需要,它可以作为进一步讨论的基础。来自 dyp、hvd 和 Columbo 的 cmets 对于查找下面提到的各种信息至关重要。

正如我所怀疑的,问题在于函数模板的部分排序规则。 [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);
}

并打印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,推导Tint,但const char* 类型的参数 匹配char[M] 类型的参数,所以#2 对于第二对类型,至少与 #1 一样专业化。
  • #2为参数,#1为参数的模板参数推导:我们发明了一个类型U来代替Tint和@ 987654346@不匹配(不同类型),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]) {}

作者认为,从直觉上讲,第二个模板应该被选为更专业化的,而且这目前不会发生(没有一个模板比另一个更专业化)。

然后他解释说原因是从const char[N] 中删除了const 限定符,产生char[N],这导致以const C* 作为参数的推导失败。

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

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


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

其中的示例相当于1610 中的示例。关于这个问题的 cmets 清楚地表明,这两个重载根据偏序算法是无序的,这正是因为在偏序期间缺乏数组到指针的衰减规则。

最后一条评论是:

有人认为有这种情况是可取的 已订购,但我们认为不值得花时间处理它 现在。如果我们在某个时候查看一些较大的偏序变化, 我们会再考虑一下。

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

【讨论】:

  • 有趣的是,如果您将foo 的参数更改为const C* const &amp;,那么它可以正常工作。
  • @Mehrdad 然而,我理解的是为什么相同的解决方案不适用于我的问题中的示例。我会说它应该是同一件事,但在那种情况下,电话仍然模棱两可。有什么想法吗?
  • 我实际上一直在忙着自己写一个答案,直到我测试了第一个解决方案并意识到(就像你所做的那样)它不起作用。我也一样难过哈哈。
  • @Mehrdad 我收回这一切。数组到指针的转换仅适用于非引用参数的模板参数推导。添加对指向第一个重载的指针的引用使其对于此调用不可行,因此只保留第二个。这可以通过将参数更改为 const C*& 来验证 - 它仍然有效,而注释掉第二个重载会导致编译错误 - '没有匹配的函数',无论有没有const
  • 在我的示例中,#1 的第二个参数不涉及模板参数推导,这就是不同之处:对于const char*&amp;,重载是不可行的(这解决了歧义,耶!);对于const char* const&amp;,我认为创建了一个临时对象,并且const&amp; 绑定到它。为什么这种转换并不比 #2 的第二个参数的直接引用绑定差,我不确定。
【解决方案2】:

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

#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: 这里还有一些代码。引入右值引用使函数更加可区分。

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

【讨论】:

  • 数组到指针的转换已在此处、问题、答案和 cmets 以及 SO 的其他地方进行了讨论。 _right 被声明为引用,这就是它没有以任何方式调整的原因。如果将其声明为const char _right[N]将等价于const char* _right(C legacy),并且无法推导出N。声明函数模板的方式,第二个、第三个和第四个is_same&lt;&gt; 测试永远不会产生true。
  • 关于编辑 1:我不会说这些功能“更容易区分”。 g() 在通过 const char[N] 时回退到 const char* 版本,这并不是我们真正想要或期望的。 h() 仅用于表明字符串文字是左值(像 7 这样的整数文字是纯右值,但不是字符串文字,它是静态存储持续时间的数组;有关详细信息,请参阅 this link)。
  • 别误会我的意思,将所有这些示例放在一个地方作为每种情况下发生情况的参考很有用。我只是在争论它们的含义和解决歧义的一般用途。请注意,上述 cmets 中讨论的解决方案确实以预期的方式解决了歧义。
猜你喜欢
  • 2017-04-24
  • 2016-01-30
  • 2015-06-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-09-28
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多