作为答案的序言,使用 Boost.Hana 可能要简单得多。 Hana 的先决条件是您的比较产生编译时答案。在您的情况下,这将需要一个包含这些基本数据类型的编译时版本的 Hana 元组,类似于std::integral_constant。如果可以接受将您的元组的值完全编码到它们的类型中,Hana 就会变得微不足道。
我相信一旦您可以在 C++20 中将元组用作非类型模板参数,就可以直接执行此操作。在那之前,您可以非常接近 (live example):
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_sequences 转换为参数包非常容易。在 C++20 之前有很长的路要走,但是在已经很难通过的代码之上有点令人眼花缭乱。
尽管元组需要 std::get 的编译时索引,但排序本身并不算太糟糕(除非您重用上述魔法,但我不得不说的是,哎呀)。您可以根据需要更改算法。您甚至可以在 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()<...>(...) 语法调用lambda。
现在我们有了按排序顺序排列的元组索引,并且魔法将 one 索引转换为 1,其值以类型编码。我没有构建处理多个值的魔法,而是采用了一种可能不太理想的方法,通过创建一个递归函数来一次构建一个对一个值的支持:
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 形式调用自身。在这种情况下,我很高兴听到 lambda 的一个良好的、与 constexpr 兼容的解决方案,它考虑到 lambda 的模板参数在每次调用时都不同。
不管怎样,这就是魔法的使用。我们想总共调用 N 次,其中 N 是元组大小。这就是if constexpr 检查的内容,最后委托给从main 传递的函数,从编译时索引顺序序列轻松构建一个新元组。为了递归,我们将此编译时索引添加到我们建立的列表中。
最后,既然应该是一个 lambda 是它自己的函数,从 main 调用的函数是一个简单的包装器,它获取索引顺序数组并从运行时序列到编译时间序列开始没有任何内容的递归开始:
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<>{});
}