如何同时遍历两个或多个容器的最佳方法?

159

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];
}

2
#include <algorithm> 中的 transform 怎么样? - Ankit Acharya
关于赋值循环:如果两者都是向量或类似的容器,使用containerA = containerB;代替循环。 - emlai
一个类似的问题:https://dev59.com/WGoy5IYBdhLWcg3wfeMR - knedlsepp
1
可能是Sequence-zip function for c++11?的重复问题。 - underscore_d
如果有人真的想要在单个循环中依次迭代两个容器,请查看https://dev59.com/vLHma4cB1Zd3GeqPJ1WM - Raven
非常适用于科学计算。为了代码的一致性,基于范围和迭代器的for循环似乎必须等待这样的应用程序。 - P. Nair
12个回答

81

来晚了。但是:我会对索引进行迭代。但不使用传统的for循环,而是使用基于范围的for循环迭代索引:

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

indices是一个简单的包装函数,它返回一组索引的(惰性求值)范围。虽然实现很简单,但代码有点长,无法在此处展示。您可以在 GitHub 上找到这个实现

这段代码的效率与手动编写的经典for循环一样高

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

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);

2
你们的索引实现与boost计数范围相比有什么优势吗?一个人可以简单地使用boost::counting_range(size_t(0), containerA.size()) - SebastianK
4
在这种情况下,最大的区别在于语法:我声称我的语法在这种情况下客观上更好。此外,您可以指定步长。请参阅链接的Github页面,特别是README文件,以获取示例。 - Konrad Rudolph
@Pixelchemist 公平地说,这里完全不需要rvalue引用(我现在会使用按值传递)。但是它也没有什么坏处:cont的值没有保存在任何地方,并且在循环内部也没有使用。 - Konrad Rudolph
错误:使用未声明的标识符“zip”:( - vaibhavatul47
@AtulVaibhav “zip 的实现留给读者作为练习”。 - Konrad Rudolph
显示剩余11条评论

46

我不明白为什么没有人提到这件事:

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;
    }
}

提示:如果容器尺寸不匹配,则您可能需要将每个容器的特定代码放入其对应的 if 块中。


1
你可以使用以下代码将其转换为for循环:auto itA = vectorA.begin(); auto itB = vectorB.begin(); for (; (itA != vectorA.end()) && (itB != vectorB.end()); (++itA, ++itB))此外,个人认为如果它们的大小不同,最好添加一个前提条件/错误提示。 - Tim
我认为这里应该使用AND而不是OR:while(itA != vectorA.end() && itB != vectorB.end()) - undefined

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

即可。
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";

点击此处查看实时示例。

然而,为了完全实现通用性,您可能需要类似这个的东西,它将正确处理数组和用户定义类型,这些类型没有成员begin()/end(),但在其命名空间中具有begin/end函数。此外,这将允许用户通过zip_c...函数明确获得const访问权限。

如果您像我一样支持友好的错误提示,则可能需要这个,它会检查是否向任何zip_...函数传递了任何临时容器,并在出现这种情况时打印友好的错误消息。


1
谢谢!不过我有一个问题,为什么你使用auto&&,它的意思是什么? - memecs
@Xeo:但是在foreach循环中,auto&&相比于auto&有什么优势呢? - Viktor Sehr
@ViktorSehr:它允许您绑定到临时元素,例如由zip_range生成的元素。 - Xeo
@ViktorSehr:auto&是一个左值引用——它根本无法绑定到临时对象。另一方面,auto&&可以绑定到临时对象或引用(左值)。 - Xeo
27
@Xeo 所有示例链接都无法打开。 - kynan
显示剩余4条评论

19

答案在这里!... 当C++23发布时。

#include <algorithm>
#include <forward_list>
#include <ranges>
#include <array>
#include <iostream>

int main()
{
    auto foos = std::to_array({ 1, 2, 3, 4, 5  });
    auto woos = std::to_array({ 6, 7, 8, 9, 10 });

    auto fooswoos = std::views::zip(foos,woos);

    for(auto [foo, woo] : fooswoos) {
        woo += foo;
    }
    std::ranges::for_each(woos, [](const auto& e) { std::cout << e << '\n'; });

    return 0;
}

那么,正在发生什么?

我们正在构建一个特殊的“视图”。这个视图允许我们查看容器,就好像它们是其他结构一样,而不需要进行任何复制或类似的操作。使用结构化绑定,我们能够在迭代中为每个对齐元素获取一个引用,并且可以对其进行任何我们想要的操作(并且是安全的)。

现在就在编译器浏览器上查看吧!</


9

有许多使用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) && (i2 != end2); ++it1, ++i2) 但编译器报错了。有人能解释一下为什么这是无效的吗? - David Doria
1
@DavidDoria for循环的第一部分是一个单独的语句。你不能在同一个语句中声明两个不同类型的变量。想一想为什么for (int x = 0, y = 0; ...)可以工作,但是for (int x = 0, double y = 0; ...)不行。 - wjl
1
然而,您可以使用std::pair<Container1::iterator, Container2::iterator>来定义变量its,并将其初始化为{c1.begin(), c2.begin()}。 - lorro
1
另一个需要注意的是,使用C++14的typename...可以轻松地将其变成可变参数。 - wjl

8

另一个解决方案可能是在 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,然后将其递增。这很容易扩展到多个容器的情况。

8

如果您需要同时迭代2个容器,boost range库中有一个扩展版本的标准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 函数。


太棒了!你是怎么发现的?似乎这并没有被记录在任何地方。 - Mikhail

8

如果可能的话,我个人更喜欢使用已经在STL(即 <algorithm>头文件中)中存在的内容。 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
使用标准库加1分!将std::back_inserter(outputContainer)作为第三个参数使用会让生活更轻松。 - marsl
1
我不喜欢这个解决方案,因为从可读性的角度来看,它看起来像是你正在将A转换成B,或者反过来。 - CraigDavid
@WilderField 我同意STL的命名不是很好,有时甚至会误导人。但我认为对于C++程序员来说,了解并熟悉这些特点是值得的。 - pooya13

6

一个区间库提供了这个以及其他非常有用的功能。以下示例使用 Boost.RangeEric Niebler的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<int&, char&>' and 'const boost::tuples::cons<const int&, boost::tuples::cons<const char&, boost::tuples::null_type> >') std::tie(ti,tc) = i; ^ - syam
将 std::tie 更改为 boost:tie 后,它编译通过了。 - syam
我使用结构化绑定版本(使用MSVC19.13.26132.0和Windows SDK版本10.0.16299.0)出现以下编译错误: “错误C2679:二进制'<<':没有找到接受右操作数类型为'const boost :: tuples :: cons < const char&,boost :: fusion :: detail :: build_tuple_cons < boost :: fusion :: single_view_iterator < Sequence,boost :: mpl :: int_ <1 >>,Last,true> :: type>'的运算符(或没有可接受的转换)”。 - pooya13
结构化绑定似乎无法与boost::combine一起使用:https://dev59.com/k1MI5IYBdhLWcg3wsNm9 - Dev Null

5

我来晚了,但是你可以使用这个(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});

或者你可以像这里一样连接向量: 什么是连接两个向量的最佳方法?,然后遍历大向量。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接