考虑到常量性、模板化性和通用性,C++ 如何解决专用模板?

How does C++ resolve specialized templates considering constness, templated-ness, and genericness?

我有下面的代码,看起来很绕但是来自真实的代码:

#include <iostream>
using namespace std;

template <class Hrm, class A>
void foo(Hrm& h, A& a)
{
  cout << "generic" << endl;
}

template <template <bool> class Hrg>
void foo(Hrg<false>& h, int& a)
{
  cout << "specialized int" << endl;
}

template <template <bool> class Hrg>
void foo(Hrg<true>& h, const int& a)
{
  cout << "specialized const-int" << endl;
}

template <bool W>
struct what;

template<> struct what<true> { };
template<> struct what<false> { };


int main() {
  what<true> wt;
  what<false> wf; 

  int i = 5;
  const int& ri = i;

  foo(wt, i);  // 1) generic
  foo(wf, i);  // 2) specialized int
  foo(wt, ri); // 5) specialized const-int
  foo(wf, ri); // 6) generic
  return 0;
}

Ideone link.

我明白了4:假Hrgconst int没有专门化,所以调用了通用版本。

我的问题是,为什么在其他情况下会调用给定的函数? 3 似乎称为专门的 const 版本,因为 const int 匹配 "more directly" 而不是 A。我想更具体地知道为什么会这样。

还有,12 呢?特别是,1 令我非常惊讶:为什么调用 generic 版本而不是专门的 const-int?


附加说明:如果我将两个 foo 专业更改为:

template <template <bool> class Hrg>
void _foo(Hrg<false>& h, int& a)
{
  cout << "specialized int" << endl;
}

template <template <bool> class Hrg>
void _foo(Hrg<true>& h, const int& a)
{
  cout << "specialized const-int" << endl;
}

template <class Hrg>
void foo(Hrg& h, int& a)
{
  return _foo(h, a);
}

template <class Hrg>
void foo(Hrg& h, const int& a)
{
  return _foo(h, a);
}

则输出变为:

foo(wt, i);     // a) specialized const-int
foo(wf, i);     // b) specialized int
foo(wt, ri);    // c) specialized const-int
//foo(wf, ri);  // d) compilation error

这对我来说是一个更直观的结果。

按顺序完成四个不同的测试用例。在所有情况下,转换序列都是相同的——不需要转换,因此我们必须继续进行重载解析的下一阶段。

foo(wt, i); // 1) generic

这里有两个潜在的过载。一切和 Hrg<true>

template <class Hrm, class A> void foo(Hrm& h, A& a);
template <template <bool> class Hrg> void foo(Hrg<true>& h, const int& a);

在[over.ics.rank]中,我们有(感谢@dyp):

Standard conversion sequence S1 is a better conversion sequence than standard conversion sequence S2 if...
S1 and S2 are reference bindings (8.5.3), and the types to which the references refer are the same type except for top-level cv-qualifiers, and the type to which the reference initialized by S2 refers is more cv-qualified than the type to which the reference initialized by S1 refers..

const intint 更符合 cv,因此 int 重载是首选 - 这将是通用的。

foo(wf, i);  // 2) specialized int

这里的两个重载是

template <class Hrm, class A> void foo(Hrm& h, A& a)
template <template <bool> class Hrg> void foo(Hrg<false>& h, int& a)

此处的两个转换序列是相同的,因此该部分中的任何内容都无法区分。所以我们继续 [over.match.best]:

Given these definitions, a viable function F1 is defined to be a better function than another viable function F2 if for all arguments i, ICSi(F1) is not a worse conversion sequence than ICSi(F2), and then
— ...
F1 and F2 are function template specializations, and the function template for F1 is more specialized than the template for F2 according to the partial ordering rules described in 14.5.6.2.

关于 "more specialized than" 的规则很复杂,但基本上意味着第二个重载对于第一个适用的类型的严格子集是可行的,因此它是首选。

foo(wt, ri); // 5) specialized const-int

在这里,我们有与第一种情况相同的两个重载:

template <class Hrm, class A> void foo(Hrm& h, A& a);
template <template <bool> class Hrg> void foo(Hrg<true>& h, const int& a);

两个转换序列相同,但第二个重载比第一个更特殊,因此首选特殊重载,原因与 (2) 中的相同。

foo(wf, ri); // 6) generic

此处 "generic" 重载是唯一可行的重载。

UPDATE 您添加的新测试比前四个更简单。给定两个 foo 重载:

template <class Hrg> void foo(Hrg& h, int& a);
template <class Hrg> void foo(Hrg& h, const int& a);

当用 ri 调用时,只有第二个重载是可行的。但是当用 i 调用时,第一个重载是首选的,原因与上面的 (1) 相同 - intconst int 更少 cv-限定].

过载解决方案发生在以下步骤中:

  1. 集合了一组候选函数。这组候选函数由非模板函数和函数模板的特化组成。如果函数模板上的模板参数推导失败,它会从候选集中以静默方式删除。
  2. 候选函数的一个子集被确定为可行。这意味着它具有的参数数量与参数数量兼容,并且每个参数都可以隐式转换为相应的参数类型。 (请注意,即使转换不明确,函数仍然可行;但在这种情况下,仍可能会选择此函数,然后您会收到编译错误)。
  3. 比较可行的功能。对于给定的一对可行函数,从某种意义上说,为了从给定参数初始化函数的参数,需要较少隐式转换的函数被认为比另一个函数 "better"。请注意,有可能给定两个函数,没有一个比另一个更好。通常,这些规则足以确定一个可行的功能优于所有其他功能。然后该函数将赢得重载决议。
  4. 如果有两个函数并且没有一个比另一个更好,那么在某些情况下 [1] 应用决胜规则可能仍然确定一个比另一个更好。如果规则 3 无法确定两个可行函数中哪个更好,但只有一个是非模板,则非模板更好;如果两者都是模板特化,但其中一个是从比另一个更专业的模板生成的,则该函数更好。在决胜局之后,如果有一个最佳可行函数(比所有其他函数都好),则该函数将赢得重载决议。如果仍然存在歧义,则重载解析失败并且调用存在歧义。

重要的是要记住第 4 步在第 3 步之后; "genericness" 或 "templateness" 是 唯一的决胜规则。

让我们回顾一下您第一个代码块中的所有示例。

(1) 第一次和第三次重载推导成功; Hrg秒推不出来。因此,候选人是第一和第三(规则 1)。两者都是可行的(规则 2)。第一个重载会将 i 绑定到 int&,而第三个重载会将 i 绑定到 const int&。最好绑定到较少的 cv 限定参考(规则 3)。 (Barry 有标准的具体引用。)第一个(通用)重载获胜。

(2) Hrg 无法推导出第三个重载,因此它不是候选者(规则 1)。第一个和第二个是候选者并且是可行的(规则 2)。第一个和第二个重载都完全匹配,不需要转换,并且根据规则 3 无法区分。第二个重载获胜,因为它更专业(规则 4)。

(5) 第二次重载推导Hrg失败,所以不是候选,而第一和第三是(规则1)。请注意,对于第一个重载,A 被推导为 const int,产生与第三个重载相同的签名。它们都是可行的(规则 2),并且在规则 3 结束时无法区分。第三个重载获胜,因为它更专业(规则 4)。

(6) 第三个重载推导Hrg失败,所以不是候选,而第一个和第二个是(规则1)。第二个重载不可行(规则 2),因为 int& 无法绑定到 ri,即 const。第一个重载,即泛型重载,是唯一可行的函数,因此它获胜。

我将重载决议留在第二个代码块中作为 reader 的练习。

[1] 作为 T.C。在评论中指出,这里有一个微妙之处。决胜规则仅适用于对于给定的一对函数,从参数初始化参数所需的隐式转换序列对于每对对应参数的排名相同的情况。如果第一个函数对一个参数有更好的隐式转换序列,而第二个函数对另一个参数有更好的隐式转换序列,则不应用决胜规则,并且歧义仍然存在。不过,问题的示例中没有出现这种情况。