我应该在这里使用std::forward还是std::move?

10

假设我有以下代码:

template<class T>
struct NodeBase
{
    T value;
    NodeBase(T &&value)
        : value(value) { }
};

然后我继承它:

template<class T>
struct Node : public NodeBase<T>
{
    Node(T &&value)
        : NodeBase( WHAT_GOES_HERE (value)) { }
};

WHAT_GOES_HERE应该使用std::forward<T>还是std::move?为什么?


1
@Mehrdad,你想要什么样的行为?你可以什么都不放,这会导致某些行为。Node<T&>是什么意思?你想从哪种对象构造它?Node<T&&>Node<T>Node<T const&>Node<T const&&>呢? - Yakk - Adam Nevraumont
1
@MooingDuck 当然: 通用引用不需要类型推导上下文。它们在类型推导/完美转发上下文之外相对无用! 在类型推导/完美转发上下文中,您想要的行为是相对明显的: 在这种情况下,它就不那么明显了。 - Yakk - Adam Nevraumont
@Mehrdad:为什么NodeBase只接受右值引用,然后从中复制?这似乎有些违反直觉,不是吗? - Mooing Duck
如果它是一个左值引用,那么应该复制它,但很少情况下会复制一个右值引用,并且 NodeBase _仅_接受右值引用。 - Mooing Duck
我在考虑 Tstd::string 的情况。在这种情况下,NodeBase 只接受 std::string&&,然后复制它。对吗? - Mooing Duck
显示剩余10条评论
3个回答

6

由于在 Node<T> 的构造函数实现中不知道 T 是一个普通类型(即非引用类型)还是一个引用类型,

std::forward<T>(value)

适用。

std::forward<T>(value) 是正确的选择,每当不确定 T && 绑定到一个 rvalue 还是 lvalue 时。这种情况在构造函数中出现,因为我们不知道 T && 是否等同于某个普通类型 UU &&,或者等同于 U & &&

无论 T 是否在使用 std::forward 的函数调用中被推导出来,或者在其他时间确定(例如,在实例化 Node 模板时确定 T),都没有关系。

std::forward<T>(value) 将以与直接由调用者调用基类构造函数相同的方式调用继承的构造函数。也就是说,当 value 是 lvalue 时,它将在 lvalue 上调用它,在 rvalue 上调用它时,value 是 rvalue。


但你是对的,std::forward<T>似乎是最好的选择:http://coliru.stacked-crooked.com/view?id=b6c7ec72b3a390dec59228e1f5e4ccd1-803889f054654ad4b92ce24ea171578e。(注意到的错误是预期的,只是实验) - Mooing Duck
U & && 是因为引用折叠而成为 U& - Mooing Duck
@MooingDuck 当然可以。我将其写成 U & && 是为了强调它由 T = U &(已声明的类型)和 &&(来自函数参数列表)组成。 - jogojapan

4
可能都不是。
我猜想你需要的是:
template<class T>
struct NodeBase
{
  T value;
  NodeBase(NodeBase &&) = default;
  NodeBase(NodeBase const&) = default; // issue: this might not work with a `T&`, but we can conditionally exclude it through some more fancy footwork
  NodeBase(NodeBase &) = default;
  template<typename U, typename=typename std:enable_if< std::is_convertible<U&&, T>::value >::type >
  NodeBase(U &&u)
    : value(std::forward<U>(u)) { }
};

template<class T>
struct Node : public NodeBase<T>
{
  Node( Node & ) = default;
  Node( Node const& ) = default; // issue: this might not work with a `T&`, but we can conditionally exclude it through some more fancy footwork
  Node( Node && ) = default;
  template<typename U, typename=typename std:enable_if< std::is_convertible<U&&, NodeBase<T>>::value >::type>
  Node(U && u)
    : NodeBase( std::forward<U>(u) ) { }
};

除非你在做某些非常奇怪的事情。
所谓的“非常奇怪”,意味着如果你的T是int,你只想要将移动的值接受到你的Node中,但如果你的T是int&,则只接受非const左值int,如果T是int const&,则接受任何可以转换为int的值。
这将是对NodeBase构造函数的奇怪要求。我可以想象出这种情况可能发生的情形,但它们并不常见。
假设你只想让NodeBase存储那些数据,那么在构造函数中使用T&&是不正确的——如果你正在存储一个int,你可能愿意复制那个int,而不仅仅是接受移动的int。
上面的代码正是这样做的——它允许任何可以存储在NodeBase中的东西被传递到该NodeBase中,进行完美转发。
另一方面,如果你确实想要奇怪的构造限制,那么这对你来说就不是正确的答案了。当我构建一个从通用引用参数构建的模板类型时,我确实想要限制传入的类型与通用引用参数完全匹配,并且仅在参数是右值引用时存储它,否则保持对它的引用。

这可能不是问题所寻求的答案,但对于在类似但更标准的情况下的任何人来说,这是一个很好的答案。+1。 - jogojapan
+1 这让我重新检查我写的内容,你很有可能是正确的! - user541686

1
在你的示例中,T没有被推导出来。 T是一个类模板参数,而不是函数模板参数。因此,假设您不会使用引用类型作为T,T&&将是对T的右值引用,因此它只能绑定到T类型的右值。这些将是安全移动的,因此您可以在此处使用std::move
template<class T>
struct Node : public NodeBase<T>
{
    Node(T &&value)
        : NodeBase( std::move (value)) { }
};

int main()
{
    int i = 3;

    Node<int> x(42); // ok
    Node<int> y(std::move(i)); // ok
    Node<int> z(i); // error
}

std::forward 通常只用于模板类型推导出的既可能是左值引用,也可能是右值引用的情况。

template<class T>
void f(T&& x)
{
    ... std::forward<T>(x) ...
}

因为T&&可能实际上是左值引用或右值引用。这仅因为在此上下文中推导出了T


@Mehrdad:没错,通常不会将引用类型用作模板参数。如果您的类使用了它,请使用std :: forward来保留引用类别。 - Andrew Tomazos
@MooingDuck,您说如果使用std::forward<T>,它会对非引用类型产生影响。您能解释一下吗?到底会出现什么问题? - jogojapan
无匹配函数调用 - Mooing Duck
@MooingDuck 是的,但这是因为 Tstd::string,因此唯一可用的构造函数是 NodeBase(std::string &&)。你只能传递一个 std::string 右值给它(而 s 不是右值)。但这是有意设计的——因为只有这一个构造函数。这不是 std::forward 的错。相反,如果 std::forward 消除了这个错误,那就是一个 bug。 - jogojapan
没错:Moved...。你也可以使用foo()来正确触发构造函数。 - Pixelchemist
显示剩余3条评论

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