我能把这句话翻译成中文:我通常/总是可以使用std::forward代替std::move吗?

66
我一直在观看Scott Meyers在C++ and Beyond 2012会议上的通用引用演讲,到目前为止一切都很清晰。然而,在大约50分钟左右,一个观众提出了一个问题,这也是我想知道的。Meyers说他不关心答案,因为这是非成语化的,会让他感到愚蠢,但我仍然很感兴趣。
所呈现的代码如下:
// Typical function bodies with overloading:
void doWork(const Widget& param)   // copy
{
  // ops and exprs using param
}
void doWork(Widget&& param)        // move
{
  // ops and exprs using std::move(param)
}

// Typical function implementations with universal reference:
template <typename T>
void doWork(T&& param)             // forward => copy and move
{
  // ops and exprs using std::forward<T>(param)
}

重点在于当我们使用rvalue引用时,我们知道它是一个rvalue,因此应该使用std::move来保留它是一个rvalue的事实。当我们使用通用引用(T&&,其中T是一个推导类型)时,我们希望std::forward保留它可能是一个lvalue或rvalue的事实。
那么问题是:既然std::forward保留了传递到函数中的值是lvalue还是rvalue,而std::move只是将其参数转换为rvalue,我们是否可以在任何地方都使用std::forward?在所有需要使用std::move的情况下,std::forward是否会像std::move一样工作,还是有一些重要的行为差异被Meyers的概括忽略了?
我并不建议任何人这样做,因为正如Meyers正确地指出的那样,这完全不符合惯用法,但以下是否也是std::move的有效用法:
void doWork(Widget&& param)         // move
{
  // ops and exprs using std::forward<Widget>(param)
}

move(lvalue) 将参数转换为 rvalue。forward(lvalue) 保留其作为 lvalue。 - Andrew Tomazos
@AndrewTomazos-Fathomling 如果 lvalue_expression 的类型是左值,则 std::forward(lvalue_expression) 将返回一个左值;如果 lvalue_expression 的类型是右值(例如命名的右值),则将返回一个右值。在这里,我正在对一个我知道具有右值类型的表达式使用 std::forward - Joseph Mansfield
1
@Andrew Tomazos - Fathomling - 这是错误的,std::forward 的整个意义在于它有时可以将左值转换为右值。 - Mankarse
@BЈовић 我不认为这是重复的。我们正在谈论同一个话题,但是提出了不同的问题。 - Joseph Mansfield
8
Scott Meyers的回复 - Joseph Mansfield
显示剩余2条评论
2个回答

73

这两个工具完全不同,但它们是互补的。

  • std::move 推导参数类型并无条件地创建一个 rvalue 表达式。这适用于实际对象或变量。

  • std::forward 需要指定模板参数(必须指定!),根据类型的不同神奇地创建了一个 lvalue 或者 rvalue 表达式(通过添加 && 和合并规则)。这只适用于推导的模板函数参数。

也许下面的例子能更好地说明这一点:

#include <utility>
#include <memory>
#include <vector>
#include "foo.hpp"

std::vector<std::unique_ptr<Foo>> v;

template <typename T, typename ...Args>
std::unique_ptr<T> make_unique(Args &&... args)
{
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));  // #1
}

int main()
{
    {
        std::unique_ptr<Foo> p(new Foo('a', true, Bar(1,2,3)));
        v.push_back(std::move(p));                                  // #2
    }

    {
        v.push_back(make_unique<Foo>('b', false, Bar(5,6,7)));      // #3
    }

    {
        Bar b(4,5,6);
        char c = 'x';
        v.push_back(make_unique<Foo>(c, b.ready(), b));             // #4
    }
}

在情况#2中,我们有一个现有的具体对象p,并且我们想要无条件地从它移动。只有std::move有意义。这里没有什么可以“转发”的。我们有一个命名变量,我们想要从中移动。
另一方面,在情况#1中,接受任何类型参数列表,并且每个参数都需要作为原始调用中相同的值类别进行转发。例如,在#3中,参数是临时表达式,因此它们将作为rvalue进行转发。但是我们也可以在构造函数调用中混合使用命名对象,如情况#4,然后我们需要将其转发为lvalues。

嘿,你还没有得到它。 :) 你需要在标签中再获得几个赞,而我仍然希望在你获得这些赞之前能够得到我的“几个答案”。 :P - Xeo
为什么会有人移动unique_ptr?我错过了什么吗? 上面的例子不是试图移动unique_ptr的存储吗?这有什么意义呢? - Gabriel
3
@Gabriel:如果没有这个操作,它就无法运行。 std::unique_ptr 无法复制。 - Ben Voigt
3
你误解了:你总是移动资源的句柄,而不是资源本身。按照定义,资源是不可移动的实体;可移动的是资源的所有权 - Kerrek SB
谢谢澄清,那是多年前我还不太确定右值引用等相关内容的时候! - Gabriel
显示剩余5条评论

14

是的,如果param是一个Widget&&,那么以下三个表达式等效(假设Widget不是引用类型):

std::move(param)
std::forward<Widget>(param)
static_cast<Widget&&>(param)

一般情况下(当“Widget”可能是一个引用时),std::move(param)等同于以下两个表达式之一:
std::forward<std::remove_reference<Widget>::type>(param)
static_cast<std::remove_reference<Widget>::type&&>(param)

注意使用 std::move 进行移动操作更加优美。而 std::forward 的重点是与模板类型推导规则很好地结合在一起:
template<typename T>
void foo(T&& t) {
    std::forward<T>(t);
    std::move(t);
}

int main() {
    int a{};
    int const b{};
               //Deduced T   Signature    Result of `forward<T>` Result of `move`
    foo(a);    //int&        foo(int&)       lvalue int          xvalue int
    foo(b);    //int const&  foo(int const&) lvalue int const    xvalue int const
    foo(int{});//int         foo(int&&)      xvalue int          xvalue int
}

除非 Widget&& 是一个推导类型,例如 template<class Widget> f(Widget&& x) - Andrew Tomazos
@Andrew Tomazos - Fathomling -- 当然。我假设Widget是一个非引用类型(就像问题中的示例)。 - Mankarse

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