10

假设我们有:

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&

4

3 回答 3

10

move,不采用右值引用,它采用被社区称为通用引用的东西。被类型推导的模板参数的行为根据引用折叠的规则。这表示:

  • 如果TK,那么T&&将只是K&&
  • 如果TK&,那么T&&将崩溃到K&
  • 如果TK&&的话,T&&就会崩溃到T&&

这就像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不返回 a的原因:如果是左值引用,T&&引用将折叠并仅返回左值引用。您确实要获得非引用类型,以便真正表示“右值引用”。Tmoveremove_reference&&

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

于 2013-01-25T23:10:07.857 回答
3

T&& 类型推导上下文中的句法形式(包括模板参数推导,但例如也推导声明为的变量的类型auto)并不表示右值引用,而是斯科特迈耶斯所说的[通用引用]。请注意,只有非常特殊的句法形式 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推断为 be并且由于引用折叠规则(变为) A&,参数的类型解析为(左值引用)。如果绑定了类型的右值,则推断为,并且参数的类型解析为(右值引用)。A&A& &&A&ATAA&&

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

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

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

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

typename remove_reference<A>::type&& std::move(A&& a);
于 2013-01-25T23:13:56.163 回答
1

尽管其他人说了什么,但该标准只讨论了右值引用。

这对 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 的通用引用概念是一种有用的方式来记住类型推导规则和引用折叠规则组合的这种令人惊讶的效果:对推导类型的右值引用可能最终成为左值引用(如果该类型可以推断为左值引用)。但是标准中没有通用参考。正如斯科特迈耶斯所说:这是一个谎言——但一个比真相更有帮助的谎言......

请注意,std::forward 对此主题有所不同:它使用额外的间接来防止参数推导(因此必须明确给出类型),但还使用引用折叠将左值作为左值转发,将右值作为右值转发。

于 2013-01-25T23:46:26.323 回答