【问题标题】:Cartesian Product using Iterators and Variadic Templates使用迭代器和可变参数模板的笛卡尔积
【发布时间】:2018-11-22 22:23:48
【问题描述】:

我正在尝试使用 STL 的样式创建一个函数来生成可变数量的输入范围的笛卡尔积。我的基本格式是该函数接受一个固定范围和一个输出范围的开始,然后是一个可变数量的双向输入迭代器。

template <
    typename BidirectionalIterator,
    typename OutputIterator,
    typename... Args
>
void cartesian_product(
    BidirectionalIterator first,
    BidirectionalIterator last,
    OutputIterator result,
    Args&&... args
);

我对@9​​87654322@ 的想法是我用它制作一个tuple,然后我遍历那个tuple 以提取元素。这需要我遵循几个基本步骤:

  1. args 创建一个tuple
  2. 取消引用新创建的tuple 中的每个迭代器
  3. 依次递增tuple 中的每个迭代器,以便我们获得范围内值的所有可能组合。

详细说明第 3 步:如果我们有两个集合 A = {0, 1} 和 B = {2, 3},笛卡尔积 A x B = {(0, 2), (0, 3), (1, 2), (1, 3)}.

我可以像这样做第一步:

auto arg_tuple = std::make_tuple(std::forward<Args>(args)...);

第二步,我不太确定。我想我会以某种方式将 push_back 元素设置为临时元组,然后将 *result 设置为等于该临时元组。 ostream 实现这一点的方式让我有点灵感,所以我认为这可以派上用场:

template <typename Tuple, typename T>
auto operator<<(const Tuple &lhs, const T &rhs)
    -> decltype(std::tuple_cat(lhs, std::make_tuple(rhs)))
{
    return std::tuple_cat(lhs, std::make_tuple(rhs));
}

第三步可能很简单。我可以结合这样的东西:

template <typename T>
auto pre_increment(T &x) -> decltype(++x) {
    return ++x;
}

这里有 for_each 的 3,000 个实现之一,用于 tuple

奇怪的是我没有正确地利用 C++14 来实现这一点。到目前为止,我的教育完全是 C++11 中难度较低的部分。

如果你想推荐我使用boost::fusion,谢谢,但我不想使用它。

【问题讨论】:

    标签: c++ c++11 c++14


    【解决方案1】:

    在 C++17 中,我们得到std::apply()。在该链接上找到了可能的 C++14 实现。然后我们可以为tuple 实现fmap

    template <class Tuple, class F>
    auto fmap(Tuple&& tuple, F f) {
        return apply([=](auto&&... args){
            return std::forward_as_tuple(f(std::forward<decltype(args)>(args))...);
        }, std::forward<Tuple>(tuple));
    }
    

    这样:

    auto deref_all = fmap(iterators, [](auto it) -> decltype(auto) { return *it; });
    auto incr_all = fmap(iterators, [](auto it) { return ++it; });
    

    【讨论】:

    • 我知道这是 OP 要求的,但我真的不明白为什么我们需要将一个包打包成一个元组只是为了解包。
    • @T.C.对于样式点(也许运气好的话,这会在不经意间帮助真正需要打包它们的人)
    • 看起来不错,但我认为这实际上会执行类似于 Python 3 的 zip 函数的操作。我不熟悉 fmap 函数(它来自 Haskell,对吗?),但似乎您已经创建了一种将每个参数单独转发的方法......作为一个元组?为什么不只是争论?编辑:没关系,那是你转发每个操作的结果。
    • @GregoryMeyer 我在答案的开头定义了fmap。我不明白这个问题...您需要所有取消引用的值,并且需要所有下一个迭代器。
    • @Barry 对不起,我的错。像往常一样,在让别人帮我弄清楚之前,我没有考虑过笛卡尔积是什么——你按照我告诉你的做得很好,但我认为我们需要做相当于嵌套的 for循环,以便完成每个可能的组合,例如 for i : a for j : b for k : c save {i, j, k}
    【解决方案2】:

    这是我想出的:

    #include <iostream>
    #include <tuple>
    #include <vector>
    
    template <typename T, typename B>
    bool increment(const B& begins, std::pair<T,T>& r) {
      ++r.first;
      if (r.first == r.second) return true;
      return false;
    }
    template <typename T, typename... TT, typename B>
    bool increment(const B& begins, std::pair<T,T>& r, std::pair<TT,TT>&... rr) {
      ++r.first;
      if (r.first == r.second) {
        r.first = std::get<std::tuple_size<B>::value-sizeof...(rr)-1>(begins);
        return increment(begins,rr...);
      }
      return false;
    }
    
    template <typename OutputIterator, typename... Iter>
    void cartesian_product(
      OutputIterator out,
      std::pair<Iter,Iter>... ranges
    ) {
      const auto begins = std::make_tuple(ranges.first...);
      for (;;) {
        out = { *ranges.first... };
        if (increment(begins, ranges...)) break;
      }
    }
    
    struct foo {
      int i;
      char c;
      float f;
    };
    
    int main(int argc, char* argv[]) {
    
      std::vector<int> ints { 1, 2, 3 };
      std::vector<char> chars { 'a', 'b', 'c' };
      std::vector<float> floats { 1.1, 2.2, 3.3 };
    
      std::vector<foo> product;
    
      cartesian_product(
        std::back_inserter(product),
        std::make_pair(ints.begin(), ints.end()),
        std::make_pair(chars.begin(), chars.end()),
        std::make_pair(floats.begin(), floats.end())
      );
    
      for (const auto& x : product)
        std::cout << x.i << ' ' << x.c << ' ' << x.f << std::endl;
    
    }
    

    cartesian_product 函数的签名与您的稍有不同,但编写包装器应该很简单。

    由于您传入的范围可能具有不同的范围,我建议您同时传递beginend,如我的示例所示。

    【讨论】:

    • 哇,我真是个傻瓜。这实际上正是我想要的。应该先编译吧?
    【解决方案3】:

    我最近提出了一种解决方案,它允许为迭代器定义的输入范围的笛卡尔积的任意组合调用可调用对象(例如 lambda)。 lambda 使输入范围的元素可以通过值或引用访问。示例用法:

    std::vector<int> vector = { 1, 2, 3 };
    std::set<double> set = { -1.0, -2.0 };
    std::string string = "abcd";
    bool array[] = { true, false };
    
    std::cout << std::boolalpha;
    cartesian_product([](const auto& v1, const auto& v2, const auto& v3, const auto& v4){
            std::cout << "(" << v1 << ", " << v2 << ", " << v3 << ", " << v4 << ")\n";
        },
        std::begin(vector), std::end(vector),
        std::begin(set), std::end(set),
        std::begin(string), std::end(string),
        std::begin(array), std::end(array)
    );
    

    我还没有找到具有这种(自然)语法(您要求的 STL 样式)的解决方案。 cartesian_product 函数在我的例子中是基于 C++17 std::apply 构建的,如下所示:

    template <typename F, typename... Ts>
    void cartesian_product_helper(F&& f, std::tuple<Ts...> t) { std::apply(f, t); }
    
    template <typename F, typename... Ts, typename Iter, typename... TailIters>
    void cartesian_product_helper(
        F&& f, std::tuple<Ts...> t, Iter b, Iter e, TailIters... tail_iters)
    {
      for (auto iter = b; iter != e; ++iter)
        cartesian_product_helper(
            std::forward<F>(f), std::tuple_cat(t, std::tie(*iter)), tail_iters...);
    }
    
    template <typename F, typename... Iters>
    void cartesian_product(F&& f, Iters... iters) {
      cartesian_product_helper(std::forward<F>(f), std::make_tuple(), iters...);
    }
    

    它相对简单 - 它递归地迭代所有范围,并且在每次迭代中,它将对相应取消引用的迭代器(即范围项)的引用附加到元组。当元组完成时(具有对所有级别项的引用),调用可调用对象并将来自元组的那些引用用作参数。

    只是我不确定这是否是最有效的方法,所以任何改进建议都会有所帮助。

    现场演示在这里:https://wandbox.org/permlink/lgPlpKXRkPuTtqo8

    【讨论】:

    猜你喜欢
    • 2017-09-28
    • 1970-01-01
    • 2021-08-13
    • 1970-01-01
    • 2012-08-27
    • 2011-04-30
    • 1970-01-01
    • 2011-01-26
    • 1970-01-01
    相关资源
    最近更新 更多