Do I use std::forward or std::move here?

2019-03-15 15:32发布

问题:

Let's say I have:

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

and I inherit from it:

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

Should WHAT_GOES_HERE be std::move or std::forward<T>? Why?

回答1:

Since in the implementation of the constructor of Node<T> it is unknown whether T is a plain type (i.e. not a reference), or a reference,

std::forward<T>(value)

is suitable.

std::forward<T>(value) is the right choice whenever it isn't known whether T && binds to an rvalue or an lvalue. This is the case here because in the constructor we don't know whether T && is equivalent to U && for some plain type U, or equivalent to U & &&.

It doesn't matter whether T is deduced in the function call that uses std::forward or determined at a different time (such as in your example, where T is determined at the time when the Node template is instantiated).

std::forward<T>(value) will call the inherited constructor in the same way as if the base class constructor had been called directly by the caller. I.e., it will call it on an lvalue when value is an lvalue, and on an rvalue when value is an rvalue.



回答2:

Probably neither.

What I suspect you should have is:

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) ) { }
};

unless you are doing something exceedingly strange.

By exceedingly strange, it means that if your T is an int, you want to only accept moved-from values into your Node, but if your T is an int&, you accept only non-const lvalue ints, and if T is an int const&, you accept any value convertible to int.

This would be a strange set of requirements to place on the constructor of NodeBase. I can think of situations where this might be the case, but they are not common.

Assuming you simply want NodeBase to store that data, taking T&& in the constructor is not the right thing to do -- if you are storing an int in NodeBase, you probably are willing to make copies of that int, instead of only accepting moved-from ints.

The above code does exactly that -- it allows anything that could be stored in the NodeBase to be passed on up to said NodeBase, with perfect forwarding.

On the other hand, if you actually want the strange set of construction restrictions, then this is not the right answer for you. I've used that when I was building the a template type that was built from a universal reference argument, and I did want to restrict the passed in type to match the universal reference argument exactly, and store it iff the argument was an rvalue reference, and otherwise keep a reference to it.



回答3:

T isn't deduced in your example. The T is a class template parameter, not a function template parameter. So assuming you will not use a reference type as T, T&& will be an rvalue-reference to T, so it will only bind to rvalues of type T. these will be safe to move so you can use std::move here.

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 is normally only for places where you have a deduced type that may either be an lvalue-reference or rvalue-reference.

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

Because T&& may actually be either an lvalue-reference or rvalue-reference. This is only because T is deduced in this context.