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?
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.
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 int
s, 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 int
s.
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.
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.