14

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:

  1. Regarding 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.
  2. Is my understanding that variant 2 should work correct?
  3. Is variant 3 supposed to work?
  4. 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.

user17732522
  • 53,019
  • 2
  • 56
  • 105
  • 1
    The problem is that suppliers of compilers somehow want to leave it open if something constexpr is constant expression or they generate run-time code for it. Programmer perhaps has to enforce it using explicit consteval and the like and usually it is rather inconvenient to pressure. Especially clang is fighting back in every way it can. If you want language lawyer answer then tag it. – Öö Tiib Dec 25 '21 at 20:33
  • @ÖöTiib I would like to have this actually work, at least in the future, but for now I am also interested in understanding the mentioned language features better. So I tagged it language-lawyer. – user17732522 Dec 25 '21 at 20:36
  • 2
    Note that `std::forward` is useless here since `ranges::size` is *always* treating the subexpression as an lvalue after [P2091](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p2091r0.pdf). – 康桓瑋 Dec 25 '21 at 22:43
  • "*If variant 3 is not correct, what is the best way to achieve my goal?*" I think the current workaround is to use `remove_reference_t::size()` just as the standard *exposition only* concept [`tiny-range`](https://eel.is/c++draft/range.lazy.split.view) does. – 康桓瑋 Dec 25 '21 at 22:58
  • Do you expect a sane type to differ on the `constexpr`-ness of `size` based on the l/r value category of the argument? What is your business case for spending effort supporting insane types? (No and none is an acceptable answer, just checking you know how academic the extra work may be) – Yakk - Adam Nevraumont Dec 26 '21 at 05:03
  • Second, within a the `constexpr` function you may not be able to use `r.size()` in a `constexpr` context, yet the value returned from itbcould be `constexpr` outside of the function. That is why `std::ranges::size` could return a `constexpr` value while taking a reference argument (in theory; I do not know the specification of std ranges size well enough to attest to it) – Yakk - Adam Nevraumont Dec 26 '21 at 05:10
  • @Yakk-AdamNevraumont I have added some clarifications to the question. I hope they answer your comments. The value category isn't really supposed to matter to my question, nor does the `constexpr` of the `is_constexpr_size` function. – user17732522 Dec 26 '21 at 10:04
  • @康桓瑋 That unfortunately doesn't cover some common cases. For example `std::array` has a non-static `size` function, so detection would fail with that. – user17732522 Dec 26 '21 at 10:49

1 Answers1

9

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.

康桓瑋
  • 33,481
  • 5
  • 40
  • 90