Consider this snippet of code, which uses the common idiom of having a function template construct an instance of a class template specialized on a deduced type, as seen with std::make_unique
and std::make_tuple
, for example:
template <typename T>
struct foo
{
std::decay_t<T> v_;
foo(T&& v) : v_(std::forward<T>(v)) {}
};
template <typename U>
foo<U> make_foo(U&& v)
{
return { std::forward<U>(v) };
}
In the context of Scott Meyers' "universal references", the argument to
make_foo
is a universal reference because its type is U&&
where U
is
deduced. The argument to the constructor of foo
is not a universal reference
because although its type is T&&
, T
is (in general) not deduced.
But in the case in which the constructor of foo
is called by make_foo
, it
seems to me that it might make sense to think of the argument to the constructor
of foo
as being a universal reference, because T
has been deduced by the
function template make_foo
. The same reference collapsing rules will apply
so that the type of v
is the same in both functions. In this case, both T
and U
can be said to have been deduced.
So my question is twofold:
foo
as being
a universal reference in the limited cases in which T
has been deduced
within a universal reference context by the caller, as in my example?std::forward
sensible?A forwarding reference is an rvalue reference to a cv-unqualified template parameter. If P is a forwarding reference and the argument is an lvalue, the type “lvalue reference to A” is used in place of A for type deduction. Hence, the two mean the same thing, and the current C++ standard term is forwarding reference.
The idiomatic use of std::forward is inside a templated function with an argument declared as a forwarding reference , where the argument is now lvalue , used to retrieve the original value category, that it was called with, and pass it on further down the call chain (perfect forwarding).
Summary. In a type declaration, “ && ” indicates either an rvalue reference or a universal reference – a reference that may resolve to either an lvalue reference or an rvalue reference. Universal references always have the form T&& for some deduced type T .
When t is a forwarding reference (a function argument that is declared as an rvalue reference to a cv-unqualified function template parameter), this overload forwards the argument to another function with the value category it had when passed to the calling function.
make_foo
is in the same ballpark as "right", but foo
isn't. The foo
constructor currently only accepts a non-deduced T &&
, and forwarding there is probably not what you mean (but see @nosid's comment). All in all, foo
should take a type parameter, have a templated constructor, and the maker function should do the decaying:
template <typename T>
struct foo
{
T v_;
template <typename U>
foo(U && u) : v_(std::forward<U>(u)) { }
};
template <typename U>
foo<typename std::decay<U>::type> make_foo(U && u)
{
return foo<typename std::decay<U>::type>(std::forward<U>(u));
}
In C++14 the maker function becomes a bit simpler to write:
template <typename U>
auto make_foo(U && u)
{ return foo<std::decay_t<U>>(std::forward<U>(u)); }
As your code is written now, int a; make_foo(a);
would create an object of type foo<int &>
. This would internally store an int
, but its constructor would only accept an int &
argument. By contrast, make_foo(std::move(a))
would create a foo<int>
.
So the way you wrote it, the class template argument determines the signature of the constructor. (The std::forward<T>(v)
still makes sense in a perverted kind of way (thanks to @nodis for pointing this out), but this is definitely not "forwarding".)
That is very unusual. Typically, the class template should determine the relevant wrapped type, and the constructor should accept anything that can be used to create the wrapped type, i.e. the constructor should be a function template.
There isn't a formal definition of "universal reference", but I would define it as:
A universal reference is a parameter of a function template with type [template-parameter]
&&
, with the intent that the template parameter can be deduced from the function argument, and the argument will be passed either by lvalue reference or by rvalue reference as appropriate.
So by that definition, no, the T&& v
parameter in foo
's constructor is not a universal reference.
However, the entire point of the phrase "universal reference" is to provide a model or pattern for us humans to think about while designing, reading, and understanding code. And it is reasonable and helpful to say that "When make_foo
calls the constructor of foo<U>
, the template parameter T
has been deduced from the argument to make_foo
in a way that allows the constructor parameter T&& v
to be either an lvalue reference or an rvalue reference as appropriate." This is close enough to the same concept that I would be fine moving on to the claim: "When make_foo
calls the constructor of foo<U>
, the constructor parameter T&& v
is essentially a universal reference."
Yes, both uses of std::forward
will do what you intend here, allowing member v_
to move from the make_foo
argument if possible or copy otherwise. But having make_foo(my_str)
return a foo<std::string&>
, not a foo<std::string>
, that contains a copy of my_str
is quite surprising....
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With