Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Detecting compile-time constantness of range size

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.

like image 820
user17732522 Avatar asked Dec 25 '21 20:12

user17732522


1 Answers

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.

like image 113
康桓瑋 Avatar answered Sep 23 '22 02:09

康桓瑋