检测范围大小的编译时常数
Detecting compile-time constantness of range size
考虑以下因素:
// 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
参数。
我的问题如下:
- 具体关于
std::ranges::size
:它通过转发引用获取其参数并转发给其他函数。出于与变体 1 相同的原因,std::ranges::size(r)
不应该永远是常量表达式(r
是常量表达式之外的局部变量)吗?如果答案是否定的,那么假设以下 std::ranges::size
被不依赖于引用的自定义实现替换。
- 我的理解是变体 2 应该正确吗?
- 变体 3 应该有效吗?
- 如果变体 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::array
、std::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_view
和 ranges::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;
}
}
考虑以下因素:
// 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
参数。
我的问题如下:
- 具体关于
std::ranges::size
:它通过转发引用获取其参数并转发给其他函数。出于与变体 1 相同的原因,std::ranges::size(r)
不应该永远是常量表达式(r
是常量表达式之外的局部变量)吗?如果答案是否定的,那么假设以下std::ranges::size
被不依赖于引用的自定义实现替换。 - 我的理解是变体 2 应该正确吗?
- 变体 3 应该有效吗?
- 如果变体 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::array
、std::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_view
和 ranges::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;
}
}