【问题标题】:Compile time sort of heterogenous tuples异构元组的编译时间排序
【发布时间】:2019-07-24 19:15:02
【问题描述】:

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

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

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

How to order types at 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));

我的假设是这是不可能的,因为您必须根据比较值的结果有条件地生成新的元组 types。就算比较功能用constexpr,好像也行不通。

然而,来自this answer 的一条随手评论表明它以某种方式可能做到这一点,只是非常困难:

我撒谎了。如果值和比较函数是,您可以这样做 constexpr,但是实现它的代码将是巨大的,不值得 是时候写了。

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

【问题讨论】:

  • 如果 C++ 模板是图灵完备的,那么答案可能是肯定的。但它可能并不漂亮。
  • 我想您可以使用索引排序来完成此操作。如果您可以得到最终索引的integer_sequence,那么您可以使用integer_sequence 以及您将在序列中的索引中使用get 的类型来构建排序类型的元组。
  • 但是创建 index_sequence 和创建元组有同样的问题……你需要根据值比较有条件地生成一个新类型。每个具有不同索引的 index_sequence 是完全不同的类型

标签: c++


【解决方案1】:

作为答案的序言,使用 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()&lt;...&gt;(...) 语法调用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<>{});
}

【讨论】:

    【解决方案2】:

    我相信,这是不可能的。

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

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

    【讨论】:

      【解决方案3】:

      返回类型不能依赖于函数的参数值(更何况参数不能是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, "!");
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-03-26
        • 2012-11-12
        • 1970-01-01
        • 2010-10-29
        相关资源
        最近更新 更多