compiler explorer link
Consider the following:
// Variant 1
template<auto> struct require_constexpr;
template<typename R>
constexpr auto is_constexpr_size(R&& r) {
return requires { typename require_constexpr<std::ranges::size(std::forward<R>(r))>; };
}
static_assert(!is_constexpr_size(std::vector{1,2,3,4}));
static_assert(is_constexpr_size(std::array{1,2,3,4}));
The goal here is not the is_constexpr_size
function as such, but to find an (requires
) expression determining that the size of a range's type is a compile-time constant, so that it can be used in a function taking any range by forwarding-reference in order to if constexpr
switch based on it.
Unfortunately this doesn't work since r
is of reference type and not usable in a constant expression, although for std::array
the call to std::range::sizes
will never access the referenced object.
Variant 2: Replacing R&&
with R
in the function parameter changes this. The constant expression requirements for non-reference type variables are weaker and both MSVC and GCC accept the code with this change, but Clang still doesn't. My understanding is that there is currently a proposal to change the rules, so that the variant with R&&
will also work as expected.
However, until this is implemented, I am looking for an alternative, not requiring restriction of the parameter to non-reference types. I also don't want to depend on the range's type being e.g. default-constructible. Therefore I cannot construct a temporary object of the correct type. std::declval
is also not usable, since std::ranges::size
needs to be evaluated.
I tried the following:
// Variant 3
return requires (std::remove_reference_t<R> s) { typename require_constexpr<std::ranges::size(std::forward<R>(s))>; };
This is accepted by MSVC, but not Clang or GCC. Reading the standard, I am not sure whether this use of a requires
parameter is supposed to be allowed.
My questions are as follows:
std::ranges::size
specifically: It takes its argument by forwarding-reference and forwards to some other function. Shouldn't std::ranges::size(r)
never be a constant expression (with r
a local variable outside the constant expression) for the same reason as in variant 1? If the answer is that it isn't, then assume for the following that std::ranges::size
is replaced by a custom implementation not relying on references.Clarification: That the references are forwarding and that I use std::forward
shouldn't be relevant to the question. Maybe I shouldn't have put them there. It is only relevant that the function takes a reference as parameter.
The use case is something like this:
auto transform(auto&& range, auto&& f) {
// Transforms range by applying f to each element
// Returns a `std::array` if `std::range::size(range)` is a constant expression.
// Returns a `std::vector` otherwise.
}
In this application the function would take a forwarding reference, but the check for compile-time constantness shouldn't depend on it. (If it does for some reason I am fine with not supporting such types.)
It is also not relevant to my question that is_constexpr_size
is marked constexpr
and used in a constant expression. I did so only for the examples to be testable at compile-time. In practice is_constexpr_size
/transform
would generally not be used in a constant expression, but even with a runtime argument transform
should be able to switch return types based on the type of the argument.
If you look closely at the specification of ranges::size
in [range.prim.size], except when the type of R
is the primitive array type, ranges::size
obtains the size of r
by calling the size()
member function or passing it into a free function.
And since the parameter type of transform()
function is reference, ranges::size(r)
cannot be used as a constant expression in the function body, this means we can only get the size of r
through the type of R
, not the object of R
.
However, there are not many standard range types that contain size information, such as primitive arrays, std::array
, std::span
, and some simple range adaptors. So we can define a function to detect whether R
is of these types, and extract the size from its type in a corresponding way.
#include <ranges>
#include <array>
#include <span>
template<class>
inline constexpr bool is_std_array = false;
template<class T, std::size_t N>
inline constexpr bool is_std_array<std::array<T, N>> = true;
template<class>
inline constexpr bool is_std_span = false;
template<class T, std::size_t N>
inline constexpr bool is_std_span<std::span<T, N>> = true;
template<auto>
struct require_constant;
template<class R>
constexpr auto get_constexpr_size() {
if constexpr (std::is_bounded_array_v<R>)
return std::extent_v<R>;
else if constexpr (is_std_array<R>)
return std::tuple_size_v<R>;
else if constexpr (is_std_span<R>)
return R::extent;
else if constexpr (std::ranges::sized_range<R> &&
requires { typename require_constant<R::size()>; })
return R::size();
else
return std::dynamic_extent;
}
For the custom range type, I think we can only get its size in a constant expression by determining whether it has a static size()
function, which is what the last conditional branch did. It is worth noting that it also applies to ranges::empty_view
and ranges::single_view
which already have static size()
functions.
Once this size detection function is completed, we can use it in the transform()
function to try to get the size value in a constant expression, and choose whether to use std::array
or std::vector
as the return value according to whether the return value is std::dynamic_extent
.
template<std::ranges::input_range R, std::copy_constructible F>
constexpr auto transform(R&& r, F f) {
using value_type = std::remove_cvref_t<
std::indirect_result_t<F&, std::ranges::iterator_t<R>>>;
using DR = std::remove_cvref_t<R>;
constexpr auto size = get_constexpr_size<DR>();
if constexpr (size != std::dynamic_extent) {
std::array<value_type, size> arr;
std::ranges::transform(r, arr.begin(), std::move(f));
return arr;
} else {
std::vector<value_type> v;
if constexpr (requires { std::ranges::size(r); })
v.reserve(std::ranges::size(r));
std::ranges::transform(r, std::back_inserter(v), std::move(f));
return v;
}
}
Demo.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With