Какой лучший способ перебрать два или более контейнеров одновременно


114

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

1
как насчет transformприсутствия #include <algorithm>?
Анкит Ачарья

О цикле присваивания: если оба являются векторами или похожими, используйте containerA = containerB;вместо цикла.
emlai 06

Похожий вопрос: stackoverflow.com/questions/8511035/…
knedlsepp

1
Возможный дубликат функции Sequence-zip для с ++ 11?
underscore_d

Ответы:


53

Скорее поздно на вечеринку. Но: я бы перебирал индексы. Но не с классическим 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 counting_range? Можно просто использоватьboost::counting_range(size_t(0), containerA.size())
SebastianK

3
@SebastianK Самая большая разница в этом случае - синтаксис: мой (я утверждаю) объективно лучше использовать в этом случае. Кроме того, вы можете указать размер шага. Примеры см. На связанной странице Github и, в частности, в файле README.
Конрад Рудольф

Ваша идея очень хороша, и я пришел к использованию counting_range только после того, как увидел ее: clear upvote :) Однако мне интересно, дает ли она дополнительную ценность для (повторной) реализации этого. Например, по производительности. Синтаксис лучше, согласен, конечно, но достаточно было бы написать простую функцию генератора, чтобы компенсировать этот недостаток.
SebastianK

@SebastianK Я признаю, что когда я писал код, я считал его достаточно простым, чтобы жить изолированно без использования библиотеки (и это так!). Теперь я бы, наверное, написал его как оболочку для Boost.Range. Тем не менее, производительность моей библиотеки уже оптимальна. Я имею в виду, что использование моей indicesреализации дает вывод компилятора, который идентичен использованию ручных forциклов. Никаких накладных расходов нет.
Конрад Рудольф

Поскольку я все равно использую boost, в моем случае было бы проще. Я уже написал эту оболочку для диапазона ускорения: все, что мне нужно, - это функция с одной строкой кода. Однако мне было бы интересно, оптимальна ли и производительность диапазонов наддува.
SebastianK

38

Для вашего конкретного примера просто используйте

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функции в их имен. Кроме того, это позволит пользователю специально получить constдоступ через zip_c...функции.

И если вы, как и я, сторонник хороших сообщений об ошибках, то вам, вероятно, понадобится это , которое проверяет, были ли переданы какие-либо временные контейнеры какой-либо из zip_...функций, и выводит красивое сообщение об ошибке, если да.


1
Спасибо! Один вопрос: почему вы используете auto &&, что это означает &&?
memecs

@memecs: я рекомендую прочитать этот вопрос , а также этот мой ответ, который вроде как объясняет, как выполняется дедукция и свертывание ссылок. Обратите внимание, что он autoработает точно так же, как параметр шаблона, и T&&в шаблоне есть универсальная ссылка, как объяснено в первой ссылке, поэтому auto&& v = 42будет выводиться как, int&&а auto&& w = v;затем будет выводиться как int&. Это позволяет вам сопоставлять lvalue, а также rvalue, и позволять изменять оба значения без создания копии.
Xeo

@Xeo: Но в чем преимущество auto && перед auto & в цикле foreach?
Виктор Сер

@ViktorSehr: он позволяет вам привязываться к временным элементам, например, созданным zip_range.
Xeo

23
@Xeo Все ссылки на примеры битые.
kynan

34

Интересно, почему об этом никто не упомянул:

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.


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;
  Container1::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)но компилятор орет. Может ли кто-нибудь объяснить, почему это неверно?
Дэвид Дориа

@DavidDoria Первая часть цикла for - это один оператор. Вы не можете объявить две переменные разных типов в одном операторе. Подумайте, почему for (int x = 0, y = 0; ...работает, а for (int x = 0, double y = 0; ...)нет.
wjl 04

1
.. Вы можете, однако, иметь std :: pair <Container1 :: iterator, Container2 :: iterator> its = {c1.begin (), c2.begin ()};
lorro

1
Следует также отметить, что это можно легко сделать вариативным с помощью C ++ 14typename...
wjl

8

В случае, когда вам нужно выполнять итерацию одновременно только по 2 контейнерам, в библиотеке диапазонов ускорения есть расширенная версия стандартного алгоритма 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.


Чудесно! Как ты нашел? Вроде нигде не задокументировано.
Михаил

4

другое решение может заключаться в захвате ссылки на итератор другого контейнера в лямбда-выражении и использовании для этого оператора пост-инкремента. например, простая копия будет:

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

внутри лямбды вы можете делать все, что угодно, itaа затем увеличивать его. Это легко распространяется на случай нескольких контейнеров.


3

Библиотека диапазонов предоставляет эту и другие очень полезные функции. В следующем примере используется Boost.Range . 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

Я получаю следующую ошибку компиляции для версии со структурированной привязкой (с использованием 19.13.26132.0версии MSVC и Windows SDK 10.0.16299.0): error C2679: binary '<<': no operator found which takes a right-hand operand of type '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>' (or there is no acceptable conversion)
pooya13

структурированные привязки, похоже, не работают с boost::combine: stackoverflow.com/q/55585723/8414561
Dev Null

2

Я тоже немного опоздал; но вы можете использовать это (вариативная функция в стиле 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});

или вы можете объединить векторы, как здесь: Как лучше всего объединить два вектора? а затем перебрать большой вектор.


0

Вот один вариант

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);    
    }
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.