c++templatesc++17move-semanticsperfect-forwarding

how to do perfect forwarding when the template argument is explicitly declared


Consider this simple function

template <typename U>
auto mkVector(U&& x0)
{
    return std::vector<std::decay_t<U>>{std::forward<U>(x0)};
}

and 4 possible use cases where the argument is either an l-value or an r-value, and the type is either implicitly stated or deducted from the arguments:

    const string lvalue("hello");

    // type inferred from arguments
    auto v1 = mkVector(lvalue);              // argument is a lvalue
    auto v2 = mkVector(string{});            // argument is a rvalue

    // type is explicilty stated
    auto v3 = mkVector<string>(lvalue);       // argument is a lvalue
    auto v4 = mkVector<string>("");           // argument is a rvalue

The case v3 fails to compile, because U is explicitly declared U=string, therefore U&& means string&& and lvalue is not compatible.

Is there a way to write the function mkVector so that it works in all possible cases correctly supporting perfect forwarding?

The best I could come up with is to write two overloads of the function, but that is not ideal and if there are N arguments instead of 1, would require 2^N possible overloads.

template <typename U, std::enable_if_t<std::is_rvalue_reference_v<U>, bool> = true>
auto mkVector(U&& x0)
{
    return std::vector<U>{std::move(x0)};
}

template <typename U>
auto mkVector(const U& x0)
{
    return std::vector<U>{x0};
}

Solution

  • You ideally want something like this:

    template <typename T = std::decay_t<U>, typename U>
    auto mkVector(U&& x0) {
        return std::vector<T>{std::forward<U>(x0)};
    }
    

    ... Where T defaults to std::decay_t<U> if not explicitly specified. However this doesn't work as a default can't refer to a later argument.

    You can use a "placeholder" type that you know won't be explicitly specified, like void, and replace it with the default if it's the placeholder

    template<typename T = void, typename U>
    auto mkVector(U&& x0) {
        using type = std::conditional_t<std::is_void_v<T>, std::decay_t<U>, T>;
        return std::vector<type>{ std::forward<U>(x0) };
    }
    

    I don't know what you mean by "would require 2^N possible overloads". If you had more arguments, you couldn't explicitly specify the second argument without explicitly specifying the first, so at most you would need N+1 overloads. For multiple arguments, you can do something like this:

    namespace detail {
    template<std::size_t N, typename Default, typename... Types>
    auto nth_or_impl() {
        if constexpr (sizeof...(Types) > N) {
            return std::tuple_element<N, std::tuple<Types...>>{};
        } else {
            return std::enable_if<true, Default>{};
        }
    }
    }
    
    template<std::size_t N, typename Default, typename... Types>
    using nth_or = typename decltype(detail::nth_or_impl<N, Default, Types...>)_)::type;
    
    template <typename... T, typename U0, typename U1, typename U2>
    auto f(U0&& x0, U1&& u1, U2&& u2) {
        static_assert(sizeof...(T) <= 3, "f: Explicitly specified more than 3 template arguments");
        using T1 = nth_or<0, std::decay_t<U0>, T...>;
        using T2 = nth_or<1, std::decay_t<U1>, T...>;
        using T3 = nth_or<2, std::decay_t<U2>, T...>;
        // ...
    }
    

    This also supports parameter packs of unknown size.