13

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:

  • Does it make sense to think of the argument to the constructor of 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?
  • In my example, are both uses of std::forward sensible?
Oktalist
  • 14,336
  • 3
  • 43
  • 63

2 Answers2

15

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.

Kerrek SB
  • 464,522
  • 92
  • 875
  • 1,084
  • alternatively, use `std::move(v)` inside the original constructor – TemplateRex Jun 30 '14 at 19:19
  • @TemplateRex: I don't want to call it an "alternative", since it's blatantly not what the OP wanted (since then the maker wouldn't make sense accepting lvalue references). – Kerrek SB Jun 30 '14 at 19:20
  • 5
    The constructor of `foo` accepts an lvalue reference. Using `std::forward` might make sense in this case. – nosid Jun 30 '14 at 19:22
  • @nosid: Ah, well spotted. Absolutely right. OK, I'm assuming that the OP doesn't want that (since the OP wants the `foo` constructor to be a universal reference), but it's possible. (Also interesting is to consider `foo`.) – Kerrek SB Jun 30 '14 at 19:26
  • 1
    In the C++11 case, do you need `return foo<...>(...)` or doesn't simply `return { ... }` (as in the question) work? (making the advantage of the C++14 version minimal) – leemes Jun 30 '14 at 19:30
  • Isn't there a rule that template-constructors are never instantiated for copy- and move-construction? – Deduplicator Jun 30 '14 at 19:31
  • @Deduplicator: A copy/move constructor is never a template. – Kerrek SB Jun 30 '14 at 19:31
  • Never said I wanted the `foo` constructor to take a universal reference in general (although it is nicer that way). Your answer demonstrates the "right way" to do what I wanted, but nosid's comment was consistent with my original intent. – Oktalist Jun 30 '14 at 19:37
  • 1
    @Oktalist: Yeah, I expanded on that. You *could* do it, but it would be very unusual. At best you could make that constructor private and a friend of the maker function, but what's really weird is the way your maker function creates "unpredictable" result types. – Kerrek SB Jun 30 '14 at 19:39
  • 2
    @Oktalist: For example, imagine code like `auto x = make_foo(f());`. Later, someone says, "Oh, I might need `f()`," and changes it to `auto const & y = f(); /* ... */ auto x = make_foo(y);`. That's just evil :-) So my main point is that a maker function should decay the type if it deduces a type (or take a non-deduced type argument, like `make_shared`); but never use its raw deduced argument type to determine the result type. – Kerrek SB Jun 30 '14 at 19:41
  • @KerrekSB I don't think it's just about the type, but the value categories as well. `make_foo` will preserve the value categories (and types) of the arguments and perfectly forward them. Then in the constructor `foo` the argument will always be lvalue + rvalue/lvalue reference, where an rvalue might were needed though. In other words, the forward of foo ctor is not a perfect forward since the type isn't deduced (with all the implications you guys mentioned). Great question btw. – KeyC0de Oct 01 '18 at 15:26
2

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....

aschepler
  • 70,891
  • 9
  • 107
  • 161