std::move和std::forward之间有什么区别?

195

2
如何轻松思考,并在何时使用哪个。当您想要移动一个值时,请使用 move,当您想要使用完美转发时,请使用 forward。这并不是什么高深的科学 ;) - Nicol Bolas
1
move() 执行无条件转换,而 forward() 根据传递的参数执行转换。 - RaGa__M
4个回答

185

std::move 接受一个对象,并允许您将其视为临时对象(rvalue)。虽然这不是语义要求,但通常接受 rvalue 引用的函数会使其无效。当您看到 std::move 时,它表示对象的值在之后不应再被使用,但您仍然可以分配一个新值并继续使用它。

std::forward 只有一个用例:将模板化函数参数(在函数内部)转换为调用者用于传递它的值类别(lvalue 或 rvalue)。这允许将 rvalue 参数作为 rvalue 传递,并将 lvalue 作为 lvalue 传递,这种方案称为“完美转发”。

说明

void overloaded( int const &arg ) { std::cout << "by lvalue\n"; }
void overloaded( int && arg ) { std::cout << "by rvalue\n"; }

template< typename t >
/* "t &&" with "t" being template param is special, and  adjusts "t" to be
   (for example) "int &" or non-ref "int" so std::forward knows what to do. */
void forwarding( t && arg ) {
    std::cout << "via std::forward: ";
    overloaded( std::forward< t >( arg ) );
    std::cout << "via std::move: ";
    overloaded( std::move( arg ) ); // conceptually this would invalidate arg
    std::cout << "by simple passing: ";
    overloaded( arg );
}

int main() {
    std::cout << "initial caller passes rvalue:\n";
    forwarding( 5 );
    std::cout << "initial caller passes lvalue:\n";
    int x = 5;
    forwarding( x );
}

正如Howard所提到的,这两个函数都将其转换为引用类型,因此它们也有相似之处。但在这些特定用例之外(这些用例涵盖了rvalue引用转换的99.9%的有用性),您应该直接使用static_cast并编写一个好的解释来说明您正在做什么。

我不确定std::forward的唯一用例是完美转发函数参数,这可能不准确。我遇到过其他情况需要完美转发,例如对象成员。 - Geoff Romer
@GeoffRomer 参数的成员?你有例子吗? - Potatoswatter
我在另一个问题中发布了一个示例:https://dev59.com/V2Ij5IYBdhLWcg3wHhtX - Geoff Romer
1
嗯,如果我理解正确的话,这意味着我可以编写一个单一函数 forward(),其中 forward(5) 就像传递了一个值一样操作 5,而 forward(x) 就像传递引用一样操作 x,而不必显式地编写重载。 - iheanyi
2
@iheanyi 是的,那就是我们的想法!但它必须是一个模板,而不仅仅是普通函数。 - Potatoswatter
显示剩余6条评论

68

std::forwardstd::move都只是类型转换。

X x;
std::move(x);

上面的代码将类型为X的左值表达式x转换为类型为X的右值表达式(确切地说是xvalue) 。move还可以接受一个右值:

std::move(make_X());

在这种情况下,它是一个身份函数:接受类型为X的右值并返回类型为X的右值。

使用std::forward可以在一定程度上选择目标:

X x;
std::forward<Y>(x);
将类型为 X 的左值表达式 x 转换为类型为 Y 的表达式。对 Y 可以使用的约束有限。
Y 可以是 X 的可访问基类,或者是 X 的基类的引用。Y 可以是 X,或者是 X 的引用。使用 forward 不能去除 cv 限定符,但可以添加 cv 限定符。Y 不能仅通过可访问基类转换从 X 转换而来。
如果 Y 是左值引用,则结果将是左值表达式。如果 Y 不是左值引用,则结果将是右值(准确地说是 xvalue)表达式。
forward 只能在 Y 不是左值引用时接受右值参数。也就是说,无法将右值强制转换为左值。出于安全原因,这样做通常会导致悬挂引用。但将右值转换为右值是可以的且被允许的。
如果您尝试指定不允许的 Y,错误将在编译时捕获,而非运行时。

如果我使用std::forward完美地将一个对象转发到函数中,那么在该函数执行后,我可以使用该对象吗?我知道,在std::move的情况下,这是未定义的行为。 - iammilind
1
关于move: https://dev59.com/rGw05IYBdhLWcg3wykzn#7028318。对于`forward`,如果传递一个左值引用,你的API应该像接收到左值引用一样做出反应。通常这意味着该值将不被修改。但是如果它是一个非const左值引用,则API可能已经对其进行了修改。如果传入一个右值引用,这通常意味着你的API可能已经从中移动,因此https://dev59.com/rGw05IYBdhLWcg3wykzn#7028318 适用。 - Howard Hinnant

22

我认为比较两个示例实现可以深入了解它们的用途和区别。

让我们从std::move开始。

std::move

简而言之:std::move用于将任何东西转换为右值(¹),以使其看起来像一个临时对象(即使它不是:std::move(non_temporary)),以便可以从中窃取其资源,即从中移动(前提是没有被const属性阻止;是的,右值可以是const的,这种情况下无法从中窃取资源)。

std::move(x)表示嗨,伙计们,注意一下,我将把这个x交给的人可以随意使用和拆分它,因此通常将其用于右值引用参数,因为您确定它们绑定到临时对象。

这是一个C++14版本的std::move实现,非常类似于Scott Meyers在Effective Modern C++中展示的(在书中,返回类型std::remove_reference_t<T>&&被改为decltype(auto),它从return语句中推导出来)。
template<typename T>
std::remove_reference_t<T>&& move(T&& t) {
    return static_cast<std::remove_reference_t<T>&&>(t);
}

从这个我们可以观察到关于std::move的以下内容:
  • 这是一个模板函数,因此可以在任何类型T上工作;
  • 它通过通用(或转发)引用T&&接收其唯一参数,因此可以操作左值和右值;T将相应地被推断为左值引用或非引用类型;
  • 模板类型推断已经生效,因此您不必通过<…>指定模板参数,在实践中,您应该永远不要指定它;
  • 这也意味着std::move只是一个使用根据推断出的非模板参数类型自动确定的模板参数的static_cast
  • 它通过使用右值引用类型(而不是非引用类型)作为返回类型,返回一个右值,而不进行任何复制;它通过使用std::remove_reference_tT中去除任何引用性,然后添加&&来实现。

小知识

你知道吗,在我们谈论的<utility>中的std::move之外,还有另一个吗?是的,它就是<algorithm>中的std::move,它做了一个半相关的事情:它是std::copy的一个版本,不是将值从一个容器复制到另一个容器,而是移动它们,使用<utility>中的std::move;所以它是使用另一个std::movestd::move

std::forward

简而言之:std::forward用于将函数内的参数转发到另一个函数,并告诉后者前者是否使用临时对象调用。

std::forward<X>(x)有两种含义:

  • (如果x绑定到一个rvalue,即临时对象) 嗨,函数先生,我从另一个函数那里收到了这个包裹,在你处理完它后,他不再需要它,所以请随意对待它
  • (如果x绑定到一个lvalue,即非临时对象) 嗨,函数先生,我从另一个函数那里收到了这个包裹,在你处理完它后,他还需要它,所以请不要破坏它

因此,通常在转发/通用引用上使用它,因为它们可以绑定到临时对象和非临时对象。

换句话说,std::forward用于能够转换这段代码

template<typename T>
void wrapper(T&& /* univ. ref.: it binds to lvalues as well as rvalues (temporaries)*/ t) {
    // here `t` is an lvalue, so it doesn't know whether it is bound to a temporary;
    // `T` encodes this missing info, but sadly we're not making `some_func` aware of it,
    // therefore `some_func` will not be able to steal resources from `t` if `t`
    // is bound to a temporary, because it has to leave lvalues intact
    some_func(t);
}

转换成这个
template<typename T>
void wrapper(T&& /* univ. ref.: it binds to lvalues as well as rvalues (temporaries)*/ t) {
    // here `t` is an lvalue, so it doesn't know whether it is bound to a temporary;
    // `T` encodes this missing info, and we do use it:
    // `t` bound to lvalue => `T` is lvalue ref => `std::forward` forwards `t` as lvalue
    // `t` bound to rvalue => `T` is non-ref    => `std::forward` turns `t` into rvalue
    some_func(std::forward<T>(t));
}

这是同一本书中的C++14实现的std::forward
template<typename T>
T&& forward(std::remove_reference_t<T>& t) {
    return static_cast<T&&>(t);
}

从这个我们可以观察到关于 std::forward 的以下内容:
  • 这是一个模板函数,因此适用于任何类型T
  • 它通过左值引用传递到无引用T的唯一参数;请注意,由于引用折叠(参见这里),std::remove_reference_t<T>&解析为与T&完全相同的内容;然而...
  • ...之所以使用std::remove_reference_t<T>&而不是T&正是为了将T放在无法推导的上下文中(参见这里),从而禁用模板类型推导,因此您必须通过<…>来指定模板参数
  • 这也意味着std::forward只是一个使用模板参数(通过引用折叠)自动确定的static_cast
  • 它通过使用rvalue引用或lvalue引用类型(而不是非引用类型)作为返回类型,返回rvalue或lvalue,而不进行任何复制;它依靠应用于T&&的引用折叠,其中T是您传递给std::forward的模板参数:如果T是非引用类型,则T&&是rvalue引用,而如果T是lvalue引用,则T&&也是lvalue引用;
  • 有些人使用static_cast<T&&>代替std::forward<T>,因为它们是等效的,并且可以节省一个实例化,但代码不够清晰,可能无法捕获错误

¹ Scott Meyers 在《Effective Modern C++》中明确地说道:

std::move 无条件地将其参数转换为右值


长话短说:std::move用于将任何东西转换为rvalue。那不是100%正确的。它不能将“任何东西”都转换为可移动或可窃取的rvalue。如果参数中存在const属性,则std::move不会改变该事实,然后您将无法移动结果。 - Nawaz
1
那是我的评论。请仔细看。我没有完全引用它,因为它很冗长,而且评论的字符数有限制。我只是在括号中重新表述了它:“(可以移动或被盗)”,你可能忽略了这一点。 - Nawaz
好的,现在添加了一些单词。 - Enlico
1
现在看起来很棒!感谢您的编辑。已点赞。 :-) - Nawaz
谢谢您促使我改进它。是的,我没有注意到您第一条评论中括号里的那部分。 - Enlico
显示剩余6条评论

21

std::forward用于将参数以与传递给函数时完全相同的方式进行“转发”。 就像这里所示:

何时使用std :: forward转发参数?

使用std::move将对象作为右值提供,以可能匹配移动构造函数或接受右值的函数。即使x本身不是右值,也会对std::move(x)执行此操作。


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