编译时排序的异构元组

Compile time sort of heterogenous tuples

本文关键字:异构 元组 排序 编译      更新时间:2023-10-16

我知道可以使用C++类型系统从现有的元组类型生成排序类型列表。

可以在以下位置找到执行此操作的示例:

https://codereview.stackexchange.com/questions/131194/selection-sorting-a-type-list-compile-time

如何在编译时对类型进行排序?

但是,是否可以按值对异构元组进行编译时排序? 例如:

constexpr std::tuple<long, int, float> t(2,1,3);
constexpr std::tuple<int, long, float> t2 = tuple_sort(t);
assert(t2 == std::tuple<int, long, float>(1,2,3));

我的假设是这是不可能的,因为您必须根据比较值的结果有条件地生成新的元组类型。 即使比较函数使用constexpr,这似乎也行不通。

然而,这个答案的随口评论表明,以某种方式可以做到这一点,只是非常困难:

我撒谎了。如果值和比较函数是 constexpr,但完成它的代码将是巨大的,不值得 是时候写了。

那么这个评论正确吗? 考虑到C++类型系统的工作方式,这在概念上怎么可能。

作为答案的序言,使用Boost.Hana可能要简单得多。Hana 的先决条件是比较生成编译时答案。在您的情况下,这将需要一个 Hana 元组,其中包含这些基本数据类型的编译时版本,类似于std::integral_constant。如果可以接受将元组的值完全编码为元组的类型,那么 Hana 就让这变得微不足道。


我相信一旦您可以在 C++20 中使用元组作为非类型模板参数,就可以直接执行此操作。在那之前,你可以非常接近(现场示例):

int main() {
constexpr std::tuple<long, int, float> t(2,1,3);
call_with_sorted_tuple(t, [](const auto& sorted) {
assert((sorted == std::tuple<int, long, float>(1,2,3)));
});
}

据我所知,直接返回排序后的元组是不可能的;回调方法是必需的,因为它是用每个可能的元组类型实例化的,并且实际上只运行正确的元组类型。这意味着此方法存在大量的编译时开销。编译时间随着元组大小的增大而快速增长。

现在,这实际上是如何工作的?让我们把魔力抛在脑后 — 将运行时整数值转换为编译时整数值。这可以很好地放入自己的标头中,并且从 P0376 中无耻地窃取:

// http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0376r0.html
#include <array>
#include <type_traits>
#include <utility>
// A function that invokes the provided function with
// a std::integral_constant of the specified value and offset.
template <class ReturnType, class T, T Value, T Offset, class Fun>
constexpr ReturnType invoke_with_constant_impl(Fun&& fun) {
return std::forward<Fun>(fun)(
std::integral_constant<T, Value + Offset>());
}
// Indexes into a constexpr table of function pointers
template <template <class...> class ReturnTypeDeducer,
class T, T Offset, class Fun, class I, I... Indices>
constexpr decltype(auto) invoke_with_constant(Fun&& fun, T index,
std::integer_sequence<I, Indices...>) {
// Each invocation may potentially have a different return type, so we
// need to use the ReturnTypeDeducer to figure out what we should
// actually return.
using return_type
= ReturnTypeDeducer<
decltype(std::declval<Fun>()(std::integral_constant<T, Indices + Offset>()))...>;
return std::array<return_type(*)(Fun&&), sizeof...(Indices)>{
{{invoke_with_constant_impl<return_type, T, Indices, Offset, Fun>}...}}
[index - Offset](std::forward<Fun>(fun));
}
template <class T, T BeginValue, T EndValue>
struct to_constant_in_range_impl {
// Instantiations of "type" are used as the Provider
// template argument of argument_provider.
template <class U>
struct type
{    
template <template <class...> class ReturnTypeDeducer, class Fun, class Self>
static constexpr decltype(auto) provide(Fun&& fun, Self&& self) {
return invoke_with_constant<ReturnTypeDeducer, T, BeginValue>(
std::forward<Fun>(fun),
std::forward<Self>(self).value,
std::make_index_sequence<EndValue - BeginValue>());
}
U&& value;
};
};

现在需要注意的一件事是,我使用 C++20 的功能来提供 lambda 模板参数,仅仅是因为编译器已经支持这一点,这使得将index_sequence转换为参数包变得非常容易。在 C++20 之前可以使用很长的方法来做到这一点,但在已经很难通过的代码之上有点碍眼。

尽管元组需要编译时索引来std::get,但排序本身还不错(除非您重用上述魔法,但我只能说 yikes)。您可以根据需要更改算法。您甚至可以在 C++20 中使用常规std::vector并将索引推到背面。我选择做的是生成一个包含元组排序索引的std::array

// I had trouble with constexpr std::swap library support on compilers.
template<typename T>
constexpr void constexpr_swap(T& a, T& b) {
auto temp = std::move(a);
a = std::move(b);
b = std::move(temp);
}
template<std::size_t I>
using index_c = std::integral_constant<std::size_t, I>;
template<typename... Ts>
constexpr auto get_index_order(const std::tuple<Ts...> tup) {
return [&]<std::size_t... Is>(std::index_sequence<Is...> is) {
std::array<std::size_t, sizeof...(Is)> indices{Is...};
auto do_swap = [&]<std::size_t I, std::size_t J>(index_c<I>, index_c<J>) {
if (J <= I) return;
if (std::get<I>(tup) < std::get<J>(tup)) return;
constexpr_swap(indices[I], indices[J]);
};
auto swap_with_min = [&]<std::size_t I, std::size_t... Js>(index_c<I> i, std::index_sequence<Js...>) {
(do_swap(i, index_c<Js>{}), ...);
};
(swap_with_min(index_c<Is>{}, is), ...);
return indices;
}(std::index_sequence_for<Ts...>{});
}

这里的主要思想是获取从 0 到 N-1 的索引包,然后单独处理每个索引。我没有尝试生成从 I+1 到 N-1 的第二个包,而是采取了简单的方法,重用了我已经拥有的 0 到 N-1 包,在交换时忽略了所有无序组合。与index_c共舞是为了避免通过笨拙的lambda.template operator()<...>(...)语法调用lambdas。

现在我们有按排序顺序排列的元组索引,并神奇地将一个索引转换为一个索引,其值编码在类型中。我没有构建处理多个值的魔术,而是采用了可能次优的方法,通过制作递归函数来一次构建对一个值的支持:

template<typename... Ts, typename F, std::size_t... Converted>
constexpr void convert_or_call(const std::tuple<Ts...> tup, F f, const std::array<std::size_t, sizeof...(Ts)>& index_order, std::index_sequence<Converted...>) {
using Range = typename to_constant_in_range_impl<std::size_t, 0, sizeof...(Ts)>::template type<const std::size_t&>;
if constexpr (sizeof...(Converted) == sizeof...(Ts)) {
f(std::tuple{std::get<Converted>(tup)...});
} else {
Range r{index_order[sizeof...(Converted)]};
r.template provide<std::void_t>([&]<std::size_t Next>(index_c<Next>) {
convert_or_call(tup, f, index_order, std::index_sequence<Converted..., Next>{});
}, r);
}
}

我会将其设为 lambda 以避免重复捕获,但作为它的递归,它需要一个解决方法来以 lambda 形式调用自己。在这种情况下,我很高兴听到一个好的、与 constexpr 兼容的 lambda 解决方案,它考虑到 lambda 的模板参数每次调用都不同。

无论如何,这是魔法的使用。我们想称之为总共 N 次,其中 N 是元组大小。这就是if constexpr检查的内容,最后委托给从main传递的函数,轻松地从编译时索引顺序序列构建一个新的元组。为了递归,我们将这个编译时索引添加到我们构建的列表中。

最后,由于应该是lambda的是它自己的函数,因此从main调用的函数是一个简单的包装器,它获取索引顺序数组并开始运行时序列到编译时间序列递归,没有转换为start:

template<typename... Ts, typename F>
constexpr void call_with_sorted_tuple(const std::tuple<Ts...>& tup, F f) {
auto index_order = get_index_order(tup);
convert_or_call(tup, f, index_order, std::index_sequence<>{});
}

我相信,这是做不到的。

任何排序的基本部分都是在上下文中使用元组值if constexpr但由于函数参数不是 constexpr,因此它们不能出现在if constexpr中。

由于元组不能是非类型模板参数,因此也无法实现基于模板的解决方案。除非我们制作类型编码值的元组(如std::integral_constant),否则我相信解决方案不可用。

返回类型不能依赖于函数的参数值(甚至更像参数不能constexpr),所以

constexpr std::tuple<long, int, float> t1(2, 1, 3);
constexpr std::tuple<long, int, float> t2(3, 2, 1);
static_assert(std::is_same<decltype(tuple_sort(t1), decltype(tuple_sort(t2)>::value, "!");