范围适配器的编译时大小
与标准不同,think-cell 的范围库已经原生支持编译时大小,因此我渴望尝试那里的习惯用法,看看它在实践中的效果如何。
namespace tc {
template <typename Rng>
constexpr auto size(Rng&& rng); // runtime-size of a range, like std::ranges::size
template <typename Rng> requires tc::has_constexpr_size<Rng>
constexpr auto constexpr_size = …; // compile-time size of a range given its type
}
tc::size
和tc::constexpr_size
我们有两种方法来查询大小:一种size
返回整数(如 )的函数std::ranges::size
,以及一种constexpr_size
变量模板,用于确定给定范围类型的编译时大小。在后一种情况下,我们可以通过创建类型constexpr_size
的变量模板来直接应用该习惯用法std::integral_constant
。这样用户就可以在界面中拥有充分的灵活性:
template <typename Rng>
void foo(Rng&& rng) {
std::size_t runtime_size = tc::size(rng);
constexpr std::size_t compile_time_size = tc::constexpr_size<Rng>();
constexpr std::integral_constant compile_time_size_constant = tc::constexpr_size<Rng>;
}
tc::size
和的接口tc::constexpr_size
剩下的就是tc::constexpr_size
. 现状是使用特质专业化。请注意,还提供tc::constexpr_size
自动支持的实现。tc::size
template <typename Rng>
struct constexpr_size_impl; // no definition
template <typename Rng>
constexpr auto constexpr_size = constexpr_size_impl<std::remove_cvref_t<Rng>>{};
// Real implementation delegates all tuple-like types to std::tuple_size, omitted here.
template <typename T, std::size_t N>
struct constexpr_size_impl<std::array<T, N>> : std::integral_constant<std::size_t, N> {};
// more specializations
tc::constexpr_size
。我们可以static constexpr std::integral_constant
在实施中使用吗?
我想改变它以某种方式利用这个static constexpr std::integral_constant
习语。也就是说,检查作为默认实现的tc::constexpr_size
格式良好性的实现。Rng::size
然后,我们只需要特征特化来为我们无法控制的类型提供实现,例如std::array
. 虽然它对于尺寸总是已知的范围工厂非常有效,例如std::array
、tc::empty_range
或tc::all_values<Enum>
,但它不适用于范围适配器。
考虑tc::transform_adaptor
(our std::ranges::transform_view
),它返回一个范围,其元素是通过应用函数转换的基础范围元素。至关重要的是,它不会更改范围的大小:如果基础范围rng
有N
元素,tc::transform(rng, fn)
则也有N
元素。因此我们希望透明地转发大小属性rng
:
- 如果
rng
具有constexpr
大小(即tc::constexpr_size<Rng>
格式良好),则 也应如此tc::transform(rng, fn)
。 - 否则,如果
rng
具有运行时大小(即tc::size(rng)
格式良好),则应该如此tc::transform(rng, fn)
。 - 否则,它既没有
constexpr
也没有运行时大小。
static constexpr std::integral_constant
使用for 的天真的尝试tc::constexpr_size
可能如下所示:
template <typename Rng, typename Fn>
struct transform_adaptor {
// for tc::constexpr_size and tc::size
static constexpr std::integral_constant size = tc::constexpr_size<Rng>; // somehow constrained
// for tc::size
constexpr std::size_t size() const
requires tc::has_size<Rng> && (!tc::has_constexpr_size<Rng>)
{
return tc::size(base_rng());
}
};
这里的问题是“某种程度上受到约束”的注释:与static
函数不同,成员变量不能受到约束,也不能用成员函数重载。constexpr
因此,只有在Rng
有大小的情况下,我们才必须有条件地继承大小constexpr
,这很丑陋。
我们也没有从使用中得到任何好处static constexpr std::integral_constant
——用户不应该直接调用rng.size()
,他们应该使用tc::size
ortc::constexpr_size
来代替。static constexpr std::integral_constant
所以在实现中使用没有任何好处。
有条件返回std::integral_constant
自.size()
因此,我们只有一个size()
函数返回std::size_t
or ,即范围大小的std::integral_constant
“最”版本。constexpr
这种方法继续适用于范围工厂,但现在我们也可以编写我们的适配器:
template <typename Rng, typename Fn>
struct transform_adaptor {
// for tc::size and tc::constexpr_size
constexpr auto size() const& requires tc::has_size<Rng> {
if constexpr (tc::has_constexpr_size<Rng>)
return tc::constexpr_size<Rng>;
else
return tc::size(base_rng());
}
};
相应的特化tc::constexpr_size
调用里面的函数decltype
来得到结果:
// Rng has a size function that returns an integral_constant.
template <typename Rng> requires requires { decltype(std::declval<Rng&>().size())::value; }
struct constexpr_size_impl<Rng> : decltype(std::declval<Rng&>().size()) {};
tc::constexpr_size
专业。运行时版本tc::size
不需要更改,因为范围仍然有一个.size()
返回大小的成员函数,只需在返回类型本身中进行编码即可。
避免代码重复
虽然该方法很好,但存在一些代码重复。对于更复杂的范围(例如 ),这一点尤其明显tc::concat_adaptor
,其大小是所有子范围大小的总和:
template <typename ... Rng>
struct concat_adaptor {
constexpr auto size() const
requires (tc::has_size<Rng> && ...)
{
if constexpr ((tc::has_constexpr_size<Rng> && ...))
return std::integral_constant<std::size_t, (tc::constexpr_size<Rng>() + ...)>{};
else
return std::apply([](auto const& ... base_rng) {
return (tc::size(base_rng) + ...);
}, base_rng_tuple);
}
};
size()
为了tc::concat_adaptor
我们计算相同的值两次:一次作为 a std::integral_constant
,一次作为std::size_t
。我们可以通过使用辅助函数分割大小计算和结果包装来统一它:
template <auto Fn, typename ... Rng>
constexpr auto compute_range_adaptor_size(Rng&&... rng) {
if constexpr ((tc::has_constexpr_size<Rng> && ...)) {
auto constexpr value = Fn(tc::constexpr_size<Rng>()...);
return std::integral_constant<std::size_t, value>{};
} else {
auto const value = Fn(tc::size(std::forward<Rng>(rng))...);
return value;
}
}
template <typename ... Rng>
struct concat_adaptor {
constexpr auto size() const
requires (tc::has_size<Rng> && ...)
{
return std::apply([](auto const& ... base_rng) {
return tc::compute_range_adaptor_size<[](auto const ... n) {
return (n + ...);
}>(base_rng...);
}, base_rng_tuple);
}
};
size()
用于tc::concat_adaptor
使用tc::compute_range_adaptor_size()
助手辅助函数compute_range_adaptor_size()
获取所有子范围并计算constexpr
其大小的最大版本。然后将其传递给 lambda 来计算派生大小,并以大多数constexpr
方式返回。请注意,lambda 必须作为非类型模板参数传递,因此我们可以在上下文中使用其结果constexpr
。
concat_adaptor::size()
然后只需要compute_range_adaptor_size()
使用适当的 lambda 和子范围进行调用,它就会自动工作。
结论
范围适配器的大小可以在编译时或运行时获得。std::integral_constant
我们可以通过尽可能返回constexpr
尺寸的最新版本来轻松转发该信息。
该模式可以应用于更多情况。例如,我最近添加了对tc::filter
返回std::true_type
or的谓词的支持std::false_type
。这样,我们就可以std::tuple
使用与按值过滤运行时范围相同的机制按类型进行过滤。
https://www.think-cell.net.cn/