Given a std::ranges::range
in C++20, how can I determine the type of the values in that range?
I want to write a function that makes a std::vector
out of an arbitrary range. I'd like this function to have a nice, explicit declaration. Something like:
template<std::ranges::range Range>
std::vector<std::value_type_t<Range>> make_vector(Range const&);
The following seems to work, but the declaration is not explicit and the implementation is ugly (even ignoring that it doesn't allocate the right size up-front where possible).
template<std::ranges::range Range>
auto make_vector(Range const& range)
{
using IteratorType = decltype(std::ranges::begin(std::declval<Range&>()));
using DerefType = decltype(*std::declval<IteratorType>());
using T = std::remove_cvref_t<DerefType>;
std::vector<T> retval;
for (auto const& x: range) {
retval.push_back(x);
}
return retval;
}
Is there a canonical/better/shorter/nicer way to do this?
Let's go through this in order:
template<std::ranges::range Range>
auto make_vector(Range const& range)
This is checking if Range
is a range, but range
isn't a Range
, it's a const Range
. It's possible that R
is a range
but R const
is not, so you're not actually constraining this function properly.
The correct constraint would be:
template<typename Range>
requires std::ranges::range<Range const>
auto make_vector(Range const& range)
But then that limits you to only const-iterable ranges (which is an unnecessary restriction) and then requires you to very carefully use Range const
throughout the body (which is very easy to forget).
Both of which are why, with ranges, you'll want to use forwarding references:
template<std::ranges::range Range>
auto make_vector(Range&& range)
That'll constrain your function properly.
Next:
using IteratorType = decltype(std::ranges::begin(std::declval<Range&>()));
using DerefType = decltype(*std::declval<IteratorType>());
using T = std::remove_cvref_t<DerefType>;
There are type traits for these things directly:
using IteratorType = std::ranges::iterator_t<Range>;
using DerefType = std::iter_reference_t<IteratorType>;
Or:
using DerefType = std::ranges::range_reference_t<Range>;
But also since you want the value type, that's (as already pointed out):
using T = std::ranges::range_value_t<Range>;
Note that the value type is not necessarily just the reference type with the qualifiers removed.
Lastly:
std::vector<T> retval;
for (auto const& x: range) {
retval.push_back(x);
}
return retval;
It'll be more efficient or even more correct to forward the element into push_back
(i.e. auto&& x
and then FWD(x)
, instead of auto const& x
and x
).
Additionally you'll want to, at the very least:
if constexpr (std::ranges::sized_range<Range>) {
retval.reserve(std::ranges::size(range));
}
Since if we have the size readily available, it'll be nice to reduce the allocations to just the one.
Lastly, in C++23, make_vector(r)
can just be spelled std::ranges::to<std::vector>(r)
. This does the allocation optimization I mentioned, but can additionally be more performant since vector
's construction internally can avoid the constant checking to see if additional allocation is necessary (which is what push_back
has to do).