Який найкращий спосіб перебрати два або більше контейнерів одночасно


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

Аналогічне питання: stackoverflow.com/questions/8511035 / ...
knedlsepp

1
Можливий дублікат функції Sequence-zip для c ++ 11?
підкреслюй_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
Чи є якась перевага вашої реалізації індексів порівняно з підвищенням counting_range? Можна було б просто скористатисяboost::counting_range(size_t(0), containerA.size())
SebastianK

3
@SebastianK Найбільша різниця в цьому випадку - синтаксис: мій (я стверджую) об’єктивно краще використовувати в цьому випадку. Крім того, ви можете вказати розмір кроку. Для прикладів див. Пов'язану сторінку Github, зокрема файл README.
Конрад Рудольф

Ваша ідея дуже приємна, і я придумав використання counting_range лише після того, як побачив її: clear upvote :) Однак мені цікаво, чи надає це додаткове значення для (повторної) реалізації цього. Наприклад, щодо продуктивності. Приємніший синтаксис, я згоден, звичайно, але досить було б написати просту функцію генератора, щоб компенсувати цей недолік.
СебастьянК

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

Оскільки я все-таки використовую прискорення, у моєму випадку це буде простіше. Я вже написав цю обгортку навколо діапазону збільшення: функція з одним рядком коду - це все, що мені потрібно. Однак мені було б цікаво, чи ефективність діапазонів підвищення також оптимальна.
СебастьянК

38

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

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

Для більш загального випадку ви можете використовувати Boost.Iterator's zip_iteratorз невеликою функцією, щоб зробити його корисним у діапазоні на основі циклів. У більшості випадків це спрацює:

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
Дякую! Хоча одне питання, чому ви використовуєте автоматичне &&, що це означає &&?
memecs

@memecs: Я рекомендую прочитати це запитання , а також моя відповідь, яка ніби пояснює, як робиться дедукція та реферат. Зауважте, що autoпрацює точно так само, як параметр шаблону, а T&&в шаблоні є універсальною посиланням, як пояснено в першому посиланні, тому auto&& v = 42буде виведено як int&&і auto&& w = v;потім буде виведено як int&. Це дозволяє зіставити lvalues ​​так само, як rvalues, і нехай обидва змінюються, не створюючи копії.
Xeo

@Xeo: Але яка перевага авто && над авто та в циклі foreach?
Віктор Шер

@ViktorSehr: Це дозволяє прив'язувати до тимчасових елементів, таких як вироблені компанією zip_range.
Xeo

23
@Xeo Усі посилання на приклади порушені.
кинан

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_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 (Container1::iterator i1 = c1.begin(), Container2::iterator i2 = c2.begin(); (i1 != end1) && (i2 != end2); ++it1, ++i2)але компілятор кричить. Хтось може пояснити, чому це недійсне?
Девід Дорія

@DavidDoria Перша частина циклу for for - це єдине твердження. Ви не можете оголосити дві змінні різних типів в одному операторі. Подумайте, чому for (int x = 0, y = 0; ...працює, а for (int x = 0, double y = 0; ...)ні.
wjl

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

1
Ще одне, що слід зазначити, що це може бути легко варіативним для C ++ 14'stypename...
wjl

8

У випадку, коли вам потрібно одночасно повторювати лише два контейнери, у бібліотеці діапазону підвищення є розширена версія стандартного алгоритму 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 контейнерів в одному алгоритмі, тоді вам потрібно пограти з блискавкою.


Чудово! Як ти знайшов? Здається, це ніде не зафіксовано.
Михайло

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 . Ерік Ніблер у діапазоні3 повинен бути хорошою альтернативою.

#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; ^
сяма

Після зміни std :: tie на boost: tie, вона складена.
сіям

Я отримую таку помилку компіляції для версії зі структурованою прив'язкою (використовуючи 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.