【问题标题】:Partial ordering of function templates and non-deduced context not working in MSVC 2017函数模板的部分排序和非推断上下文在 MSVC 2017 中不起作用
【发布时间】:2017-08-19 09:33:23
【问题描述】:

identity 模板是关闭某个(依赖)模板参数的演绎的常用习惯用法,例如允许隐式转换,如下例所示:

#include <iostream>

template<class T>
struct A {};
struct B : public A<int> {};
struct C {
  operator B() { return {}; }
};

template<typename U> struct identity { typedef U type; };

template<class T> using identity_t = typename identity<T>::type;

template<class X>
void test(A<X> arg1, A<X> arg2) {              // #1
  std::cout << "ok1";
}

template<class X>
void test(A<X> arg1, identity_t<A<X>> arg2) {  // #2
  std::cout << "ok2";
}

int main() {
  B a, b;
  C c;
  test(a, b);
  test(b, c);
}

但是不同编译器的结果不一样:Live demo on Godbolt

  • GCC 6:ok1ok2
  • clang 5:ok1ok2
  • MSVC 2017:

27 : <source>(27): error C2668: 'test': ambiguous call to overloaded function
20 : <source>(20): note: could be 'void test<int>(A<int>,A<int>)'
15 : <source>(15): note: or       'void test<int>(A<int>,A<int>)'
27 : <source>(27): note: while trying to match the argument list '(B, B)'

这种错误是有道理的(尽管肯定是 MSVC 中的一个错误),因此我提出了关于为什么以及如何在 GCC 和 clang 中工作的问题:

  1. test(a, b) 如何选择#1test(b, c) - #2?至少在test(a, b) 的情况下,它们看起来同样出色。

  2. 为什么编译器不抱怨两个签名完全相同的test 实例化?

【问题讨论】:

  • 大约 2。-我猜这是因为“第一个匹配模板获胜”的逻辑。但这并不能解释 1. 根本......
  • (1.) 它们并不一样好。否则编译器会发出模棱两可的信号。 test(a, b) 将选择 #1,因为 ab 在 is-a 关系中都是 A&lt;X&gt;,而 #1 是完全匹配的。 #2 也是完全匹配的,但需要更多步骤。
  • 昨天在a related question 上,协议好像是MSVC没有完全实现偏序。

标签: c++ c++11 templates


【解决方案1】:

这是MSVC中的一个错误,程序是正确的。

test(a, b)如何选择#1

对于test(a, b),重载解析从函数调用中执行参数推导(参见[temp.deduct.call]):

  • #1 推导出为void test(A&lt;int&gt;, A&lt;int&gt;)
  • #2 推导出为void test(A&lt;int&gt;, &lt;non-deduced context&gt;),然后从arg1 合成arg2 为A&lt;int&gt;,结果:void test(A&lt;int&gt;, A&lt;int&gt;)

有不止一种可行的替代方案,因此流程继续部分排序(请参阅[temp.deduct.partial])。

部分排序使用 original 模板,尝试将一个模板的每个参数成对地从一个类型推导 ([temp.deduct.type]) 到另一个模板中(在次要之后转换),反之亦然。 如果仅在一个方向上成功推演,则将获胜模板选为最专业的。

在嵌套上下文中从类型推导总是失败(范围运算符:: 左侧的任何内容都是嵌套上下文),请参阅[temp.deduct.type]/5

未推断的上下文是:

— 使用 qualified-id 指定的类型的 nested-name-specifier

。 . .

所以这意味着#2 在偏序中总是会失败;推论总是失败,反之则总是成功:

  1. void test(A&lt;U&gt;, typename identity&lt;A&lt;U&gt;&gt;::type)推导出void test(A&lt;T&gt;,A&lt;T&gt;):P1=A&lt;T&gt;,A1=A&lt;U&gt;,P2=A&lt;U&gt;,A2=A&lt;U&gt;,成功,T=U

  2. void test(A&lt;U&gt;,A&lt;U&gt;)推导出void test(A&lt;T&gt;, typename identity&lt;A&lt;T&gt;&gt;::type):P1=A&lt;T&gt;,A1=A&lt;U&gt;,P2=&lt;non-deduced-context&gt;,失败

所以偏序的结果是:使用void test(A&lt;T&gt;,A&lt;T&gt;) (#1) 调用test(a, b)

test(b, c) 如何选择#2

对于test(b, c),不能从C 推导出A&lt;X&gt;(推导过程中不考虑隐式转换),因此#2 是唯一可行的替代方案。 identity_t&lt;A&lt;X&gt;&gt; 被解析 推演到A&lt;int&gt;,因为X 是已知的(从第一个参数推导出来)。

为什么编译器没有抱怨两个签名完全相同的测试实例?

函数声明中引用的模板参数是实例化函数签名的一部分。见[temp.over.link]

  1. 可以重载函数模板,使两个不同的函数模板特化具有相同的类型。

  2. 这些特化是不同的功能,不违反单一定义规则。

【讨论】:

    猜你喜欢
    • 2010-11-13
    • 1970-01-01
    • 1970-01-01
    • 2015-02-18
    • 2017-05-28
    • 1970-01-01
    • 2016-01-30
    • 1970-01-01
    • 2017-11-09
    相关资源
    最近更新 更多