【问题标题】:What's the best way to iterate over two or more containers simultaneously同时迭代两个或多个容器的最佳方法是什么
【发布时间】:2017-01-14 05:01:44
【问题描述】:

C++11 提供了多种迭代容器的方法。例如:

基于范围的循环

for(auto c : container) fun(c)

std::for_each

for_each(container.begin(),container.end(),fun)

但是,推荐的方法是迭代两个(或更多)相同大小的容器以完成以下操作:

for(unsigned i = 0; i < containerA.size(); ++i) {
  containerA[i] = containerB[i];
}

【问题讨论】:

标签: c++ c++11 iterator containers


【解决方案1】:

晚会有点晚了。但是:我会遍历索引。但不是使用经典的 for 循环,而是使用基于范围的 for 循环遍历索引:

for(unsigned i : indices(containerA)) {
    containerA[i] = containerB[i];
}

indices 是一个简单的包装函数,它返回索引的(惰性求值)范围。由于实现(虽然简单)有点太长,无法在此处发布,you can find an implementation on GitHub

此代码与使用手动经典 for 循环一样高效

如果这种模式经常出现在您的数据中,请考虑使用另一种模式,该模式zips 两个序列并产生一系列元组,对应于成对的元素:

for (auto& [a, b] : zip(containerA, containerB)) {
    a = b;
}

zip 的实现留给读者作为练习,但它很容易从indices 的实现中得到。

(在 C++17 之前,您必须改为编写以下代码:)

for (auto items&& : zip(containerA, containerB))
    get<0>(items) = get<1>(items);

【讨论】:

  • 与 boostcounting_range 相比,您的索引实现有什么优势吗?可以简单地使用boost::counting_range(size_t(0), containerA.size())
  • @SebastianK 在这种情况下最大的区别是语法:我的(我声称)客观上更好地用于这种情况。此外,您可以指定步长。有关示例,请参阅链接的 Github 页面,尤其是 README 文件。
  • 您的想法非常好,我在看到它之后才想到使用counting_range:clear upvote :) 但是,我想知道它是否为(重新)实现这一点提供了额外的价值。例如,关于性能。我当然同意更好的语法,但是编写一个简单的生成器函数来弥补这个缺点就足够了。
  • @SebastianK 我承认,当我编写代码时,我认为它很简单,可以在不使用库的情况下独立生活(确实如此!)。现在我可能会把它写成 Boost.Range 的包装器。也就是说,我的库的性能已经是最佳的。我的意思是,使用我的indices 实现产生的编译器输出与使用手动for 循环相同。没有任何开销。
  • 因为无论如何我都使用 boost,所以在我的情况下它会更简单。我已经围绕 boost range 编写了这个包装器:我只需要一行代码的函数。但是,如果提升范围的性能也是最佳的,我会很感兴趣。
【解决方案2】:

对于您的具体示例,只需使用

std::copy_n(contB.begin(), contA.size(), contA.begin())

对于更一般的情况,您可以使用 Boost.Iterator 的 zip_iterator,并带有一个小函数,使其可用于基于范围的 for 循环。在大多数情况下,这将起作用:

template<class... Conts>
auto zip_range(Conts&... conts)
  -> decltype(boost::make_iterator_range(
  boost::make_zip_iterator(boost::make_tuple(conts.begin()...)),
  boost::make_zip_iterator(boost::make_tuple(conts.end()...))))
{
  return {boost::make_zip_iterator(boost::make_tuple(conts.begin()...)),
          boost::make_zip_iterator(boost::make_tuple(conts.end()...))};
}

// ...
for(auto&& t : zip_range(contA, contB))
  std::cout << t.get<0>() << " : " << t.get<1>() << "\n";

Live example.

但是,对于全面的通用性,您可能需要更像this 的东西,它可以正常工作于没有成员begin()/end()的数组和用户定义类型 在其命名空间中有 begin/end 函数。此外,这将允许用户通过zip_c... 函数专门获得const 访问权限。

如果你像我一样提倡漂亮的错误消息,那么你可能想要this,它检查是否有任何临时容器被传递给任何zip_... 函数,如果出现则打印一个很好的错误消息所以。

【讨论】:

  • 谢谢!不过有个问题,你为什么要用auto&&,&&是什么意思?
  • @memecs:我建议通读this question,以及this answer of mine,这有点解释了演绎和参考折叠是如何完成的。请注意,auto 与模板参数的工作方式完全相同,并且模板中的T&amp;&amp; 是第一个链接中解释的通用引用,因此auto&amp;&amp; v = 42 将被推导出为int&amp;&amp;,然后auto&amp;&amp; w = v; 将是推断为int&amp;。它允许您匹配左值和右值,并让两者都是可变的,而无需复制。
  • @Xeo:但是在 foreach 循环中 auto&& 相对于 auto& 的优势是什么?
  • @ViktorSehr:它允许您绑定到临时元素,例如zip_range 生成的元素。
  • @Xeo 示例的所有链接都已损坏。
【解决方案3】:

我想知道为什么没有人提到这个:

auto itA = vectorA.begin();
auto itB = vectorB.begin();

while(itA != vectorA.end() || itB != vectorB.end())
{
    if(itA != vectorA.end())
    {
        ++itA;
    }
    if(itB != vectorB.end())
    {
        ++itB;
    }
}

PS:如果容器大小不匹配,那么您可能需要将每个容器特定代码放入其对应的 if 块中。

【讨论】:

    【解决方案4】:

    algorithm 标头中提供了许多方法来使用多个容器执行特定操作。例如,在您给出的示例中,您可以使用 std::copy 而不是显式的 for 循环。

    另一方面,除了普通的 for 循环之外,没有任何内置方法可以通用地迭代多个容器。这并不奇怪,因为有很多 很多 方法可以进行迭代。想一想:你可以用一个步骤迭代一个容器,用另一个步骤迭代一个容器;或通过一个容器直到它到达末端,然后在您穿过另一个容器的末端时开始插入;或者每次您完全通过另一个容器时第一个容器的一个步骤然后重新开始;或其他模式;或一次超过两个容器;等等……

    然而,如果你想让你的自己的“for_each”风格的函数迭代两个容器,直到最短的一个容器的长度,你可以这样做:

    template <typename Container1, typename Container2>
    void custom_for_each(
      Container1 &c1,
      Container2 &c2,
      std::function<void(Container1::iterator &it1, Container2::iterator &it2)> f)
      {
      Container1::iterator begin1 = c1.begin();
      Container2::iterator begin2 = c2.begin();
      Container1::iterator end1 = c1.end();
      Container2::iterator end2 = c2.end();
      Container1::iterator i1;
      Container2::iterator i2;
      for (i1 = begin1, i2 = begin2; (i1 != end1) && (i2 != end2); ++it1, ++i2) {
        f(i1, i2);
      }
    }
    

    显然,您可以用类似的方式制定任何类型的迭代策略。

    当然,您可能会争辩说,直接执行内部 for 循环比编写这样的自定义函数更容易……而且您是对的,如果您只打算执行一两次。但好的是,这是非常可重用的。 =)

    【讨论】:

    • 看来你必须在循环之前声明迭代器?我试过这个:for (Container1::iterator i1 = c1.begin(), Container2::iterator i2 = c2.begin(); (i1 != end1) &amp;&amp; (i2 != end2); ++it1, ++i2) 但编译器会大喊大叫。谁能解释为什么这是无效的?
    • @DavidDoria for 循环的第一部分是一条语句。您不能在同一语句中声明两个不同类型的变量。想想为什么for (int x = 0, y = 0; ... 有效,而for (int x = 0, double y = 0; ...) 无效。
    • .. 但是,您可以拥有 std::pair<:iterator container2::iterator> its = {c1.begin(), c2.begin()};
    • 另外需要注意的是,这可以很容易地用 C++14 的typename... 进行可变参数化
    【解决方案5】:

    如果您只需要同时迭代 2 个容器,boost 范围库中有标准 for_each 算法的扩展版本,例如:

    #include <vector>
    #include <boost/assign/list_of.hpp>
    #include <boost/bind.hpp>
    #include <boost/range/algorithm_ext/for_each.hpp>
    
    void foo(int a, int& b)
    {
        b = a + 1;
    }
    
    int main()
    {
        std::vector<int> contA = boost::assign::list_of(4)(3)(5)(2);
        std::vector<int> contB(contA.size(), 0);
    
        boost::for_each(contA, contB, boost::bind(&foo, _1, _2));
        // contB will be now 5,4,6,3
        //...
        return 0;
    }
    

    当您需要在一个算法中处理超过 2 个容器时,您需要使用 zip。

    【讨论】:

    • 太棒了!你怎么找到的?似乎它没有记录在任何地方。
    【解决方案6】:

    另一种解决方案可能是在 lambda 中捕获另一个容器的迭代器的引用,并在其上使用后增量运算符。例如简单的副本是:

    vector<double> a{1, 2, 3};
    vector<double> b(3);
    
    auto ita = a.begin();
    for_each(b.begin(), b.end(), [&ita](auto &itb) { itb = *ita++; })
    

    在 lambda 中,您可以使用 ita 执行任何操作,然后将其递增。这很容易扩展到多个容器的情况。

    【讨论】:

      【解决方案7】:

      范围库提供了这个和其他非常有用的功能。以下示例使用Boost.RangeEric Niebler's rangev3 应该是一个不错的选择。

      #include <boost/range/combine.hpp>
      #include <iostream>
      #include <vector>
      #include <list>
      
      int main(int, const char*[])
      {
          std::vector<int> const v{0,1,2,3,4};
          std::list<char> const  l{'a', 'b', 'c', 'd', 'e'};
      
          for(auto const& i: boost::combine(v, l))
          {
              int ti;
              char tc;
              boost::tie(ti,tc) = i;
              std::cout << '(' << ti << ',' << tc << ')' << '\n';
          }
      
          return 0;
      }
      

      C++17 将通过结构化绑定使这一点变得更好:

      int main(int, const char*[])
      {
          std::vector<int> const v{0,1,2,3,4};
          std::list<char> const  l{'a', 'b', 'c', 'd', 'e'};
      
          for(auto const& [ti, tc]: boost::combine(v, l))
          {
              std::cout << '(' << ti << ',' << tc << ')' << '\n';
          }
      
          return 0;
      }
      

      【讨论】:

      • 这个程序不是用 g++ 4.8.0 编译的。 delme.cxx:15:25: error: no match for 'operator=' (operand types are 'std::tuple&lt;int&amp;, char&amp;&gt;' and 'const boost::tuples::cons&lt;const int&amp;, boost::tuples::cons&lt;const char&amp;, boost::tuples::null_type&gt; &gt;') std::tie(ti,tc) = i;^
      • 将std::tie改为boost:tie后,编译成功。
      • 对于带有结构化绑定的版本(使用 MSVC 19.13.26132.0 和 Windows SDK 版本 10.0.16299.0),我收到以下编译错误:error C2679: binary '&lt;&lt;': no operator found which takes a right-hand operand of type 'const boost::tuples::cons&lt;const char &amp;,boost::fusion::detail::build_tuple_cons&lt;boost::fusion::single_view_iterator&lt;Sequence,boost::mpl::int_&lt;1&gt;&gt;,Last,true&gt;::type&gt;' (or there is no acceptable conversion)
      • 结构化绑定似乎不适用于boost::combinestackoverflow.com/q/55585723/8414561
      【解决方案8】:

      我也有点晚了;但你可以使用这个(C 风格的可变参数函数):

      template<typename T>
      void foreach(std::function<void(T)> callback, int count, ...) {
          va_list args;
          va_start(args, count);
      
          for (int i = 0; i < count; i++) {
              std::vector<T> v = va_arg(args, std::vector<T>);
              std::for_each(v.begin(), v.end(), callback);
          }
      
          va_end(args);
      }
      
      foreach<int>([](const int &i) {
          // do something here
      }, 6, vecA, vecB, vecC, vecD, vecE, vecF);
      

      或者这个(使用函数参数包):

      template<typename Func, typename T>
      void foreach(Func callback, std::vector<T> &v) {
          std::for_each(v.begin(), v.end(), callback);
      }
      
      template<typename Func, typename T, typename... Args>
      void foreach(Func callback, std::vector<T> &v, Args... args) {
          std::for_each(v.begin(), v.end(), callback);
          return foreach(callback, args...);
      }
      
      foreach([](const int &i){
          // do something here
      }, vecA, vecB, vecC, vecD, vecE, vecF);
      

      或者这个(使用大括号括起来的初始化列表):

      template<typename Func, typename T>
      void foreach(Func callback, std::initializer_list<std::vector<T>> list) {
          for (auto &vec : list) {
              std::for_each(vec.begin(), vec.end(), callback);
          }
      }
      
      foreach([](const int &i){
          // do something here
      }, {vecA, vecB, vecC, vecD, vecE, vecF});
      

      或者你可以像这里一样加入向量:What is the best way to concatenate two vectors?,然后遍历大向量。

      【讨论】:

        【解决方案9】:

        如果可能,我个人更喜欢使用 STL 中已有的内容(在 &lt;algorithm&gt; 标头中)。 std::transform 有一个可以接受两个输入迭代器的签名。因此,至少对于两个输入容器的情况,您可以这样做:

        std::transform(containerA.begin(), containerA.end(), containerB.begin(), outputContainer.begin(), [&](const auto& first, const auto& second){
            return do_operation(first, second);
        });
        

        请注意,outputContainer 也可以是输入容器之一。但一个限制是,如果您正在修改其中一个容器,则无法执行更新后操作。

        【讨论】:

        • +1 用于使用标准库!使用std::back_inserter(outputContainer) 作为第三个参数让生活更轻松。
        【解决方案10】:

        这是一种变体

        template<class ... Iterator>
        void increment_dummy(Iterator ... i)
            {}
        
        template<class Function,class ... Iterator>
        void for_each_combined(size_t N,Function&& fun,Iterator... iter)
            {
            while(N!=0)
                {
                fun(*iter...);
                increment_dummy(++iter...);
                --N;
                }
            }
        

        示例用法

        void arrays_mix(size_t N,const float* x,const float* y,float* z)
            {
            for_each_combined(N,[](float x,float y,float& z){z=x+y;},x,y,z);    
            }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2015-09-03
          • 2012-04-22
          • 2020-09-27
          • 1970-01-01
          • 2010-12-18
          相关资源
          最近更新 更多