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