Search code examples
c++c++11language-lawyerperfect-forwardinguniversal-reference

Is this a universal reference? Does std::forward make sense here?


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?

Solution

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