范围适配器的编译时大小

发布时间 2023-11-03 14:43:37作者: 51component技术交流

与标准不同,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::sizetc::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::arraytc::empty_rangetc::all_values<Enum>,但它不适用于范围适配器。

考虑tc::transform_adaptor(our std::ranges::transform_view),它返回一个范围,其元素是通过应用函数转换的基础范围元素。至关重要的是,它不会更改范围的大小:如果基础范围rngN元素,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::sizeortc::constexpr_size来代替。static constexpr std::integral_constant所以在实现中使用没有任何好处。

有条件返回std::integral_constant.size()

因此,我们只有一个size()函数返回std::size_tor ,即范围大小的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_typeor的谓词的支持std::false_type。这样,我们就可以std::tuple使用与按值过滤运行时范围相同的机制按类型进行过滤。

https://www.think-cell.net.cn/