检测范围大小的编译时常数

Detecting compile-time constantness of range size

compiler explorer link

考虑以下因素:

// 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}));

这里的目标不是 is_constexpr_size 函数本身,而是找到一个 (requires) 表达式来确定范围类型的大小是一个编译时常量,以便它可以在通过转发引用获取任何范围的函数中使用,以便 if constexpr 基于它进行切换。

不幸的是,这不起作用,因为 r 是引用类型,不能用在常量表达式中,尽管对于 std::array,对 std::range::sizes 的调用将永远不会访问引用的对象.

变体 2:在函数参数中将 R&& 替换为 R 会改变这一点。非引用类型变量的常量表达式要求较弱,MSVC 和 GCC 都接受了这种变化的代码,但 Clang 仍然不接受。我的理解是,目前有 a proposal 来更改规则,因此具有 R&& 的变体也将按预期工作。

但是,在这个实现之前,我正在寻找一个替代方案,不需要将参数限制为非引用类型。我也不想依赖范围的类型,例如默认可构造。因此我无法构建正确类型的临时对象。 std::declval 也不可用,因为 std::ranges::size 需要评估。

我尝试了以下方法:

// Variant 3

return requires (std::remove_reference_t<R> s) { typename require_constexpr<std::ranges::size(std::forward<R>(s))>; };

这是 MSVC 接受的,但 Clang 或 GCC 不接受。阅读标准,我不确定是否应该允许使用 requires 参数。

我的问题如下:

  1. 具体关于 std::ranges::size:它通过转发引用获取其参数并转发给其他函数。出于与变体 1 相同的原因,std::ranges::size(r) 不应该永远是常量表达式(r 是常量表达式之外的局部变量)吗?如果答案是否定的,那么假设以下 std::ranges::size 被不依赖于引用的自定义实现替换。
  2. 我的理解是变体 2 应该正确吗?
  3. 变体 3 应该有效吗?
  4. 如果变体 3 不正确,实现我的目标的最佳方法是什么?

澄清:参考文献是转发的,我使用 std::forward 应该与问题无关。也许我不应该把它们放在那里。唯一相关的是该函数将引用作为参数。

用例是这样的:

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.
}

在此应用程序中,该函数将采用转发引用,但编译时常量检查不应依赖于它。 (如果出于某种原因确实如此,我可以不支持此类类型。)

is_constexpr_size 被标记为 constexpr 并在常量表达式中使用也与我的问题无关。我这样做只是为了在编译时可以测试示例。在实践中 is_constexpr_size/transform 通常不会在常量表达式中使用,但即使使用运行时参数 transform 也应该能够根据类型切换 return 类型参数。

仔细看[range.prim.size]ranges​::​size的说明,除了R的类型是原始数组类型,ranges​::​size获取的大小r 通过调用 size() 成员函数或将其传递给自由函数。

并且由于transform()函数的参数类型是reference,所以ranges::size(r)在函数体中不能作为常量表达式,也就是说我们只能通过R类型获取r的大小,而不是对象R

但是,包含大小信息的标准范围类型并不多,例如原始数组、std::arraystd::span和一些简单的范围适配器。所以我们可以定义一个函数来检测R是否属于这些类型,并通过相应的方式从它的类型中提取大小

#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;
}

对于自定义范围类型,我认为我们只能通过确定它是否具有静态size()函数来获取常量表达式中的大小,这是最后一个条件分支所做的。值得注意的是,它也适用于 ranges::empty_viewranges::single_view 已经具有静态 size() 功能。

这个尺寸检测功能完成后,我们可以在transform()函数中使用它来尝试获取常量表达式中的尺寸值,并选择使用std::array还是std::vector作为return值根据return值是否为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.