为什么const数组优先绑定到const T&参数而不是T &&参数?

5

一般来说,重载一个带有 "T&&" 参数的函数模板是不明智的,因为它可以与任何东西绑定,但让我们假设我们仍然这样做:

template<typename T>
void func(const T& param)
{
  std::cout << "const T&\n";
}

template<typename T>
void func(T&& param)
{
  std::cout << "T&&\n";
}

我的理解是const T&重载将被用于常量左值参数,T&&重载将被用于其他所有参数类型。但是考虑调用包含常量和非常量内容数组的func时会发生什么:
int main()
{
  int array[5] = {};
  const int constArray[5] = {};

  func(array);             // calls T&& overload
  func(constArray);        // calls const T& overload
}

VC10、VC11和gcc 4.7在展示的结果上达成了一致。我的问题是为什么第二次调用会引起const T&重载。简单的答案是constArray里有一个const,但我认为这太简单了。不管选择哪个模板,被推断出来的类型T都是“含5个const int的数组”,因此const T&重载中param的类型将是“含5个const int的const数组的引用”。但是名为constArray的数组本身没有被声明为const。那么为什么对func(constArray)的调用不会引起T&&重载,从而产生param类型为“含5个const int的数组的引用”呢?
这个问题是由与问题相关的讨论激发出来的,链接为c++模板函数参数推断和函数解析,但我认为该线程被其他问题偏离了轨道,并没有澄清我现在在这里提出的问题。

那是因为引用折叠! - Nawaz
2个回答

7
在函数参数列表(以及其他任何地方),数组类型上的cv限定符被移动到右侧以限定数组元素类型。例如,对于 T = int [5]const T & 被转换为 int const (&) [5]

3.9.3 CV-qualifiers [basic.type.qualifier]

2 - [...] 应用于数组类型的任何cv限定符都会影响数组元素类型,而不是数组类型(8.3.4)。

因此,使用类型为 int const [5] 的参数调用 func 会被推断为以下两种调用之一:
void func<int [5]>(int const (&) [5])
void func<int const (&) [5]>(int const (& &&) [5])
// where the above collapses to
// 'void func<int const (&) [5]>(int const (&) [5])'

这两种重载方法都可行,但首选前者:

假设 T1 是 const T & 模板,T2 是 T && 模板;也就是说,它们的参数类型分别是 T1 := const T & 和 T2 := T &&。然后,可以将它们转换为合成类型 CD 的参数类型,如下所示(14.5.6.2:3):A1 := const C &,A2 := D &&

现在,我们尝试使用 A1 作为参数模板和 P2 作为参数模板对 T1 和 T2 进行排序(14.8.2.4:2)。我们删除引用(14.8.2.4:5),得到 A1 -> const C 和 T2 -> T,然后删除 cv 限定符(14.8.2.4:7),得到 A1 -> C 和 T2 -> T。模板 T 可以被推导为 C(14.8.2.4:8),因此A1 至少与 P2 同等专业;反过来,A2 -> D -> D,P1 -> const T -> T,且 T 可以被推导为 D,因此A2 至少与 P1 同等专业

这通常意味着两者都不比另一个更专业;然而,由于 PA 类型都是引用类型,因此适用 14.8.2.4:9,由于 A1 是左值引用而 P2 不是,因此认为 T1 比 T2 更为专业(在相同条款下,引用类型之间的平局也可以通过 cv 限定符来打破)。


感谢您提供标准中涉及此问题的参考资料。我一直在查看初始化规则,但这显然不是正确的地方。(我现在发现这些信息实际上在8.3.4/1中。) - KnowItAllWannabe
两个重载函数具有相同的签名。为什么一个更可取?答案必须与模板特化的部分排序有关,但我很难将这个论点拼凑在一起。 - Kerrek SB
@KerrekSB:我同意,如果能将这个问题详细说明一下就更好了,因为两个模板都可以接受const和非const的左值和右值。直觉上,T&&版本似乎比const T&版本不太专业化,这与我引用的编译器的行为一致,但更少含糊的解释会非常有帮助。 - KnowItAllWannabe
1
@KnowItAllWannabe:我需要退后一步 - 我认为部分排序实际上并没有涉及(因为两个模板具有相同数量的模板参数);相反,这是一个(更简单的)问题,即哪个特化更加专业化。 - Kerrek SB
@KerrekSB,关键规则是14.8.2.4:9(右值引用不如左值引用专业)。我已经运行了上面的部分排序算法。 - ecatmur
@ecatmur:太棒了,非常好!这实际上非常复杂。感谢您的详细解释! - Kerrek SB

3
你混淆了右值引用(例如 int&&)和通用引用(由模板参数生成,例如 template <typename T> ... T&&)。
右值引用确实无法绑定到左值。但通用引用可以绑定到任何东西。问题只是谁更匹配。
你拥有的类型是 int const [5]。现在让我们看看:
  • T const & 匹配:与 T = int[5] 匹配。

  • T && 匹配:与 T = int const (&)[5] 匹配。

前者是更好的匹配,以下是原因:两个模板都产生相同的重载。但是 T = int[5]T = int const (&)[5] 更具体化。这是因为你可以将 T = int const (&)[5] 实现为 T = U const &,其中 U = int[5]
请注意,要将一个左值绑定到通用引用,类型本身必须被推断为引用类型。
(显然,array 无法匹配 const T &,因为它不是 const。它只能匹配 T&&,推断出 T = int (&)[5])。

显然前者更匹配!你认为这很明显吗?我不这么认为。 - Nawaz
@Nawaz: "Clearly" = "有人应该查一下扣除排名规则"。但是,两者之间存在CV转换的差异,这会将“1”添加到指标中。 - Kerrek SB
@Nawaz:真正的问题是T &&T const &&之间谁会获胜:我认为绑定到引用计数为立即匹配,所以我的赌注是这将是模棱两可的。 - Kerrek SB
为什么T要被推断为int[5]?const不是顶层的,因此不应该被剥离,我认为T应该被推断为const int [5]以适用于const T&模板。我同意您对于T &&模板的推断类型是const int(&)[5]。你能解释一下为什么你认为适用于数组内容的const(*而不是数组本身)将在类型推断过程中被剥离吗? - KnowItAllWannabe
2
@知道一切的自以为是者:啊,我误解了你的问题。好的,你已经从其他答案中得到了答案。但是这里重要的不是论证(我们已经解决了),而是哪个专业化程度更高的问题。 - Kerrek SB

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接