【问题标题】:When are two function templates considered as partially ordered and when are ambiguous?什么时候两个函数模板被认为是部分有序的,什么时候是模棱两可的?
【发布时间】:2014-10-11 18:23:37
【问题描述】:

在阅读了How to make these std::function parameters unambiguous? 的问题后,我完全糊涂了,到目前为止,我以为我理解了 函数模板的部分排序 是什么,但在阅读了该问题后,我写下了三个示例检查编译器的行为,收到的结果让我很难理解。

示例 #1

template <class T>
void foo(T) {}

template <class T>
void foo(T&) {}

int main()
{
  int i;
  foo<int>(i); // error: call is ambiguous! 
}

问题: 这两个功能都是可行的,这很明显,但是使用T&amp; 的功能不是比T 更专业吗?相反,编译器会引发 ambiguous call 错误。

示例 #2

#include <iostream>

template <class T>
struct X {};

template <>
struct X<int> 
{
  X() {}
  X(X<int&> const&) {} // X<int> is constructible from X<int&>
  // note: this is not a copy constructor!
};

template <>
struct X<int&> 
{
  X() {}
  X(X<int> const&) {} // X<int&> is constructible from X<int>
  // note: this is not a copy constructor!
};

template <class T>
void bar(X<T>) { std::cout << __PRETTY_FUNCTION__ << std::endl; }

template <class T>
void bar(X<T&>) { std::cout << __PRETTY_FUNCTION__ << std::endl; }

int main()
{
  bar<int>(X<int>());   // calls void bar(X<T>) [with T = int]

  bar<int>(X<int&>());  // calls void bar(X<T&>) [with T = int]
}

问题:如果在示例 #1 中 T&amp;T 是不明确的,那么为什么这里没有一个呼叫是不明确的? X&lt;int&gt; 可以从 X&lt;int&amp;&gt; 构造,X&lt;int&amp;&gt; 可以从 X&lt;int&gt; 构造,这要归功于提供的构造函数。是不是因为编译器生成的X&lt;int&gt;::X(X&lt;int&gt; const&amp;) copy-constructor 是一个X&lt;int&gt;::X(X&lt;int&amp;&gt; const&amp;) 更好的转换序列,(如果是这样,是什么让它变得更好,请注意参数是按值传递的),所以排序专业的数量根本不重要?

示例#3

#include <iostream>

// note: a new type used in constructors!
template <class U>
struct F {};

template <class T>
struct X
{
  X() {}

  template <class U>
  X(F<U> const&) {}  // X<T> is constructible from any F<U>
  // note: it takes F type, not X!
};

template <class T>
void qux(X<T>) { std::cout << __PRETTY_FUNCTION__ << std::endl; }

template <class T>
void qux(X<T&>) { std::cout << __PRETTY_FUNCTION__ << std::endl; }

int main()
{
  qux<int>(F<int>());  // calls void qux(X<T&>) [with T = int]

  qux<int>(F<int&>()); // calls void qux(X<T&>) [with T = int]
}

问题:现在这类似于 “将 lambda [](int){}std::function&lt;void(int&amp;)&gt;std::function&lt;void(int)&gt; 匹配”。为什么在这两个调用中都选择了更专业的函数模板?是不是因为转换顺序相同,所以偏序开始重要了?

在 GCC 4.9.0 上使用-std=c++11 完成所有测试,没有额外的标志。

【问题讨论】:

  • 你为什么用template &lt;class T&gt;而不是template &lt;typename T&gt;?第二个版本我从来没有遇到过问题
  • @msrd0 这没什么区别。 classtypename 在这种情况下是同一个意思。
  • 当您将函数转换为普通的非模板函数时,您的前两个示例同样模棱两可且明确(分别)。我感觉你让事情变得不必要地复杂了。
  • @hvd 偏序使得模板在重载解析中的行为与常规函数略有不同,这就是问题所在,为什么有时这些模板是有序的,为什么在其他情况下它们不是跨度>

标签: c++ templates language-lawyer overload-resolution partial-ordering


【解决方案1】:

重载解析试图找到这样的最佳函数:

(1) [over.match.best]/1:

鉴于这些定义,一个可行的函数F1 被定义为 如果所有参数都比另一个可行的函数F2 更好的函数 iICSi(F1) 不是比ICSi(F2) 更差的转换序列,然后
— 对于某些参数 jICSj(F1) 是更好的转换 序列比ICSj(F2),或者,如果不是,
- 上下文是 通过用户定义的转换进行初始化(见 8.5、13.3.1.5 和 13.3.1.6)以及从F1的返回类型到目标类型(即实体的类型)的标准转换序列 initialized) 是比标准更好的转换序列 从F2的返回类型到目标的转换序列 类型。
[示例:

struct A {
  A();
  operator int();
  operator double();
} a;

int i = a;    // a.operator int() followed by no conversion is better 
              // than `a.operator double()`     
              // followed by a conversion to `int`
float x = a;  // ambiguous: both possibilities require conversions,
              // and neither is better than the other

- end example ] 或者,如果不是这样,
- F1 是非模板 函数和 F2 是函数模板特化,或者,如果不是 那,
— F1 和 F2 是函数模板特化,并且 F1 的函数模板比​​ F2 的模板更专业 根据 14.5.6.2 中描述的部分排序规则。


案例一:

但是使用T&amp; 的人不是比T 更专业吗?

根据重载决议,没有转换更好(两者都是身份转换,它们是完全匹配的),并且由于 (1) 中没有其他项目符号适用,因此完成了部分排序。 [temp.deduct.partial]/5 表示引用被它们所引用的类型替换,以实现部分排序:

在完成部分排序之前,某些转换是 对用于偏序的类型执行:
— 如果 P 是 引用类型,P 被引用的类型替换。
— 如果 A 是 引用类型,A 被引用的类型替换。

由于参数模板的参数完全相同,不难看出相互推导在两种情况下都是成功的——因此两个模板都不比另一个模板更专业。

案例 2:

这里不需要部分排序。从X&lt;int&gt;X&lt;int&amp;&gt; 的用户定义转换比将X&lt;int&gt; 转换为X&lt;int&gt; 的排名更差——后者由[over.ics.user]/4 给出精确匹配排名:

类类型表达式到相同类类型的转换是 给定精确匹配排名,[…]

因此,它显然比X&lt;int&gt;X&lt;int&amp;&gt; 的转换更好,后者具有转换等级。反之亦然,X&lt;int&amp;&gt;X&lt;int&gt;

案例 3:

第三种情况与第一种类似。 X&lt;int&gt;X&lt;int&amp;&gt; 都有一个构造函数模板,可以采用F 的任意特化。 (1) 告诉我们,由于没有一个转换序列在任何方面都比另一个更好(事实上,它们是完全相同的),因此选择了更专业的模板。

template <class T> void bar(X<T>);  // #1

template <class T> void bar(X<T&>); // #2

// Call:
bar<int>( F<int>() );

回到[temp.deduct.partial],进行类型推导。为每个实参模板的模板形参综合了一个独特的类型,称为 Unique。执行以下具有相应结果的过程 - 请注意,使用F&lt;int&gt; 调用时的步骤与使用F&lt;int&amp;&gt;(以及F 的任何其他特殊化)时的步骤完全相同:

  1. 模板#1 作为参数模板,模板#2 作为参数模板,X&lt;Unique&amp;&gt;X&lt;T&gt; 进行推导,得到T=Unique&amp;。另一方面,
  2. 模板#2作为参数模板,模板#1作为参数模板,X&lt;Unique&gt;X&lt;T&amp;&gt;进行推导,导致推演失败

我们可以看到模板 #2 更加专业。当在步骤 1 中用作参数模板时,推导成功,而对于模板 #1 作为步骤 2 中的参数模板,推导失败。因此,调用了第二个更专业的函数模板的专业化。

【讨论】:

  • 所以对于 Ex1 的基本思想是:“从函数模板的每个参数的类型中删除引用和顶级 cv 限定符,用于部分排序目的”所以foo(T&amp;) 被衰减为foo(T),只是就像foo(X&lt;T&amp;&gt;&amp;) 衰减成foo(X&lt;T&amp;&gt;)。话虽如此,引用删除并不适用于类型T 本身,而是适用于一般考虑的参数类型,无论是T&amp; 还是X&lt;T&gt;&amp;,或者其他什么,是吗?
  • 正确。对于部分排序,引用和 cv 限定符对于某些步骤并不重要。
  • +1,很好的答案。对于X&lt;T&gt;X&lt;T&amp;&gt; 的情况,[temp.deduct.partial]/p12 中的示例也很重要。
【解决方案2】:

示例 1:

编译器无法知道您是想通过引用还是通过值传递a。如果你用 T * 专门化你的模板,他会很容易知道,因为函数调用语法会不同 foo(&amp;a)

示例 2:

在这里你告诉编译器qux 的第二个重载需要X&lt;T &amp;&gt;,所以他知道你想用T &amp; 构造这个对象。没有歧义。但如果你这样做:

template <class T>
void qux(X<T>) { std::cout << __PRETTY_FUNCTION__ << std::endl; }

template <class T>
void qux(X<T> &) { std::cout << __PRETTY_FUNCTION__ << std::endl; }

你最终会遇到同样的问题

示例 3:

同样的问题。

我不知道它是否很清楚,所以如果有人可以改进我的答案,那可能对作者有用

【讨论】:

  • Ex#1。但这不是void foo(int)void foo(int&amp;),而是模板,所以我希望编译器对它们进行部分排序。任何对标准的参考都会有所帮助。例#2。问题是为什么复制构造函数更好,因为它可以从X&lt;int&gt;X&lt;int&amp;&gt; 构建X&lt;int&gt;。 Ex#3 请注意,在这种情况下,在这两种情况下都选择了更专业的函数
  • 对于 Ex#1,编译器会将 T 替换为 int 所以这是同样的问题,问题是他不知道你想对你的函数调用做什么,它可能正在发送一个参考或只是一个值。对于 Ex#2,我没有看到复制构造函数部分,但我不认为这些是复制构造函数,你给它的对象类型与模板类 ` & 不同,但我可能是错的
  • 我不能同意我是否使用模板并不重要。这意味着部分排序没有意义,因为编译器只能“总是实例化所有模板并在它们上运行重载决议”。对于 Ex#2 和 Ex#3,我真的试图让它们尽可能简单(对不起,如果它们不可读)。请注意,在 Ex2 编译器中使用复制构造函数,同时它也具有转换构造函数
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-11-28
  • 1970-01-01
  • 2014-08-30
  • 2011-10-04
相关资源
最近更新 更多