如何推导 std::move 模板参数

How are std::move template parameters deduced?

本文关键字:参数 move std 何推导      更新时间:2023-10-16

假设我们有:

foo(A&& a);

如果你这样做

A a;
foo(a);

它不会编译,抱怨不能将左值绑定到 A&&。 这完全没问题。

但是,鉴于 std::move 的签名,

template<class T> typename remove_reference<T>::type&& std::move(T&& a);

看起来它需要一个右值引用,就像在 foo 中一样,为什么下面的代码符合?

A a;
std::move(a);

a不是左值吗?

此外,据说编译将实例化:

typename remove_reference<A&>::type&& std::move(A& && a);

我不明白为什么不是:

typename remove_reference<A>::type&& std::move(A && a);

在我看来a属于A型,而不是A&型。

不,

move不接受右值引用,它采用社区称为通用引用的内容。被类型推导的模板参数的行为与引用折叠规则一致。这意味着:

  • 如果TK,那么T&&将只是K&&;
  • 如果T K&,则T&&将塌陷到K&;
  • 如果T K&&,则T&&将折叠为T&&

这就像&&&的逻辑 AND,其中 & 为 0,&&为 1:

      &    &&
   |-----------|
&  |  &  |  &  |
   |-----|-----|
&& |  &  | &&  |
   |-----------|

这就是move对右值和左值的工作方式。

例子:

template<typename T>
void f(T&&);
f<int>   // T is int;   plugging int into T makes int&& which is just int&&
f<int&>  // T is int&;  plugging int& into T is int& && which collapse to int&
f<int&&> // T is int&&; plugging int&& into T is int&& && which collapse to int&&

请注意,引用折叠仅发生在模板参数上;您不能直接键入int&& &&并期望它进行编译。当然,您不会像那样手动指定类型。这些只是为了显示引用折叠到什么。

所以你真的会这样称呼它:

int i;
f(i); // T is int&;  int& && collapses to int&
f(4); // T is int&&; int&& && collapses to int&&

引用折叠也是move不返回T&&的原因:如果引用是左值引用,则引用会折叠T并使move只返回左值引用。您remove_reference获取非引用类型,以便&&真正意味着"右值引用"。

您可以在此处了解更多信息:http://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers

在类型演绎的上下文中T&&的句法形式(包括模板参数推导,但例如也包括声明为 auto 的变量类型的推导)并不表示右值引用,而是 Scott Meyers 所说的 [通用引用]。请注意,只有非常特殊的句法形式T&&表示通用引用,而其他类似的形式则不被视为通用引用。例如:

template<typename T> 
void foo(T&& t); <-- T&& is a universal reference
template<typename T> 
void foo(T const&& t); <-- T const&& is NOT a universal reference
template<typename T> 
void foo(S<T>&& t); <-- S<T>&& is NOT a universal reference
template<typename T> 
struct S { void foo(T&& t); }; <-- T&& is NOT a universal reference

通用引用可以绑定到左值和右值。如果绑定了 A 类型的左值,则T被推导出为A&,并且由于引用折叠规则(A& &&变为A&),参数的类型解析为 A&(左值引用)。如果绑定了类型 A 的右值,则推导T A,并且参数的类型解析为A&&(右值引用)。

[注意引用折叠规则可能看起来很复杂,但它们实际上很容易:引用Stephan T. Lavavej的话,"左值引用是具有传染性的",这意味着当形式T&& &T& &T& &&被实例化时,它们总是解析为T& - 只有形式T&& &&解析为T&&

这就是为什么当参数为左值时,std::move函数模板将按如下方式实例化(T被推导为T&):

typename remove_reference<A&>::type&& std::move(A& && a);

而当参数是右值时,它将按如下方式实例化(T被推导出为A

typename remove_reference<A>::type&& std::move(A&& a);

不管其他人怎么说,该标准只谈论右值引用。

对于 std::move 如何工作的关键是模板参数推导规则中明确的特殊规则:

[...]如果 [声明的函数参数类型] 是右值引用 到 CV 非限定模板参数,并且参数是左值, 类型"对 A 的左值引用"用于代替 A 表示类型 演绎。[...]

另一部分是引用折叠的规则,它说

如果 [...] 一个类型

模板参数 [...] 表示一个类型 TR,它是对 类型 T,尝试创建类型"对 cv TR 的左值引用" 创建类型"对 T 的左值引用",同时尝试创建 类型"对 cv TR 的右值引用"创建类型 TR。

现在template<class T> typename remove_reference<T>::type&& std::move(T&& a);函数参数 a 匹配上述规则("对 cv-非限定模板参数的右值引用"),因此如果参数是左值,则推导类型将是参数类型的左值引用。在您的情况下,这会导致 T = A&。

将其代入移动收益声明

remove_reference<A&>::type&& std::move<A&>(A& && a);

使用 remove_reference 的定义和引用折叠规则(对 TR => TR 的右值引用),可以得出这样的结果:

A&& std::move<A&>(A& a);

斯科特·迈耶(Scott Meyer)的通用参考概念,正如在其他答案中提出的,是一种有用的方法来记住类型推导规则和参考折叠规则组合的惊人效果:对推导类型的右值引用可能最终成为左值引用(如果该类型可能被推导出为左值引用)。但是该标准没有通用的参考。正如斯科特·迈耶斯(Scott Meyers)所说:这是一个谎言——但一个比真相更有帮助的谎言......

请注意,std::forward 是这个主题的不同转折:它使用额外的间接寻址来防止参数推导(因此必须显式给出类型),但也使用引用折叠将左值作为左值转发,将右值作为右值转发。