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:
- Regarding
std::ranges::size
specifically: It takes its argument by forwarding-reference and forwards to some other function. Shouldn'tstd::ranges::size(r)
never be a constant expression (withr
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 thatstd::ranges::size
is replaced by a custom implementation not relying on references. - Is my understanding that variant 2 should work correct?
- Is variant 3 supposed to work?
- If variant 3 is not correct, what is the best way to achieve my goal?
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.