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

use*_*522 15 c++ language-lawyer c++-concepts c++20 std-ranges

请注意,在提出此问题后,缺陷报告更改了下面提到的行为。见问题末尾。


编译器资源管理器链接

考虑以下:

// 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}));
Run Code Online (Sandbox Code Playgroud)

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

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

变体 2:在函数参数中替换为 会改变这一点R&&R非引用类型变量的常量表达式要求较弱,MSVC 和 GCC 都接受经过此更改的代码,但 Clang 仍然不接受。我的理解是,目前有一项更改规则的提案,以便 with 的变体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))>; };
Run Code Online (Sandbox Code Playgroud)

这被 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.
}
Run Code Online (Sandbox Code Playgroud)

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

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


对于未来的读者:

2022 年中期,上面链接的提案已被接受为 DR,因此,编译器一旦实施它,引用的原始问题就应该消失。

然而,关于rodr 在 - 表达式中使用时是否可用,这可能是另一个问题requires。我最初写这个问题时没有注意到这一点,所以我不想将这个问题的范围扩展到它。

康桓瑋*_*康桓瑋 10

如果仔细观察[range.prim.size]ranges\xe2\x80\x8b::\xe2\x80\x8bsize中的规范,除了当 的类型是原始数组类型时,通过调用成员函数或将其传递给自由函数来获取 的大小。Rranges\xe2\x80\x8b::\xe2\x80\x8bsizersize()

\n

并且由于函数的参数类型transform()引用ranges::size(r)不能在函数体内作为常量表达式,这意味着我们只能通过r的类型获取 的大小R,而不能通过的对象R来获取。

\n

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

\n
#include <ranges>\n#include <array>\n#include <span>\n\ntemplate<class>\ninline constexpr bool is_std_array = false;\ntemplate<class T, std::size_t N>\ninline constexpr bool is_std_array<std::array<T, N>> = true;\n\ntemplate<class>\ninline constexpr bool is_std_span = false;\ntemplate<class T, std::size_t N>\ninline constexpr bool is_std_span<std::span<T, N>> = true;\n\ntemplate<auto> \nstruct require_constant;\n\ntemplate<class R>\nconstexpr auto get_constexpr_size() {\n  if constexpr (std::is_bounded_array_v<R>)\n    return std::extent_v<R>;\n  else if constexpr (is_std_array<R>)\n    return std::tuple_size_v<R>;\n  else if constexpr (is_std_span<R>)\n    return R::extent;\n  else if constexpr (std::ranges::sized_range<R> &&\n                     requires { typename require_constant<R::size()>; })\n    return R::size();\n  else\n    return std::dynamic_extent;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

这个大小检测函数完成后,我们就可以在函数中使用它来尝试获取常量表达式中的大小值,并根据返回值是否为 来transform()选择是否使用std::arrayor作为返回值。std::vectorstd::dynamic_extent

\n
template<std::ranges::input_range R, std::copy_constructible F>\nconstexpr auto transform(R&& r, F f) {\n  using value_type = std::remove_cvref_t<\n    std::indirect_result_t<F&, std::ranges::iterator_t<R>>>;\n  using DR = std::remove_cvref_t<R>;\n  constexpr auto size = get_constexpr_size<DR>();\n  if constexpr (size != std::dynamic_extent) {\n    std::array<value_type, size> arr;\n    std::ranges::transform(r, arr.begin(), std::move(f));\n    return arr;\n  } else {\n    std::vector<value_type> v;\n    if constexpr (requires { std::ranges::size(r); })\n      v.reserve(std::ranges::size(r));\n    std::ranges::transform(r, std::back_inserter(v), std::move(f));\n    return v;\n  }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

演示。

\n