Функція sequence-zip для c ++ 11?


99

За допомогою нового циклу for на основі діапазону ми можемо писати код типу

for(auto x: Y) {}

Який ІМО є значним покращенням порівняно з (наприклад,)

for(std::vector<int>::iterator x=Y.begin(); x!=Y.end(); ++x) {}

Чи може він використовуватися для циклу через два одночасних циклу, як zipфункція Pythons ? Для тих, хто не знайомий з Python, код:

Y1 = [1,2,3]
Y2 = [4,5,6,7]
for x1,x2 in zip(Y1,Y2):
    print x1,x2

Дає як вихід (1,4) (2,5) (3,6)


На основі діапазону forможна використовувати лише одну змінну, тому ні. Якщо ви хочете отримати доступ до двох значень одночасно, вам доведеться використовувати щось на зразокstd::pair
Сет Карнегі

4
@SethCarnegie: не безпосередньо, але ви можете придумати zip()функцію, яка повертає кортежі та перебирає список кортежів.
Андре Карон,

2
@ AndréCaron ти маєш рацію, моє "ні" означало сказати, що ти не можеш використовувати дві змінні, не те, що ти не можеш повторювати відразу два контейнери.
Сет Карнегі,

Очевидно, for(;;)можна отримати таку поведінку, хоч і довгу руку, тож чи справді питання: чи можна "авто" для двох об'єктів одночасно?

У майбутньому перегляді (сподіваємось, C ++ 17) капітальний ремонт STL включатиме діапазони . Тоді view :: zip може надати найкраще рішення.
Джон Макфарлейн,

Відповіді:


88

Попередження: boost::zip_iterator і boost::combineстаном на Boost 1.63.0 (2016, 26 грудня) спричинить невизначену поведінку, якщо довжина вхідних контейнерів неоднакова (це може призвести до аварійного завершення або повторення після закінчення).


Починаючи з Boost 1.56.0 (2014, 7 серпня), яким ви могли б скористатисяboost::combine (функція існує у попередніх версіях, але недокументована):

#include <boost/range/combine.hpp>
#include <vector>
#include <list>
#include <string>

int main() {
    std::vector<int> a {4, 5, 6};
    double b[] = {7, 8, 9};
    std::list<std::string> c {"a", "b", "c"};
    for (auto tup : boost::combine(a, b, c, a)) {    // <---
        int x, w;
        double y;
        std::string z;
        boost::tie(x, y, z, w) = tup;
        printf("%d %g %s %d\n", x, y, z.c_str(), w);
    }
}

Це надрукувало б

4 7 a 4
5 8 б 5
6 9 c 6

У попередніх версіях ви могли самостійно визначити діапазон так:

#include <boost/iterator/zip_iterator.hpp>
#include <boost/range.hpp>

template <typename... T>
auto zip(T&&... containers) -> boost::iterator_range<boost::zip_iterator<decltype(boost::make_tuple(std::begin(containers)...))>>
{
    auto zip_begin = boost::make_zip_iterator(boost::make_tuple(std::begin(containers)...));
    auto zip_end = boost::make_zip_iterator(boost::make_tuple(std::end(containers)...));
    return boost::make_iterator_range(zip_begin, zip_end);
}

Використання однакове.


1
Ви могли б використовувати це для сортування? тобто std :: sort (zip (a.begin (), ...), zip (a.end (), ...), [] (tup a, tup b) {a.get <0> () > b.get <0> ()}); ?
gnzlbg


Мене спокушали б optionalелементи для попередніх можливостей ітерації ...
Якк - Адам Неврамон

3
Будь-який шанс зробити це за допомогою std :: make_tuple та std :: tie? Я намагався використовувати це, мінімізуючи залежність від посилення, але не зміг змусити це працювати.
Карнейро,

@kennytm будь-яка ідея, чому вони вирішили піти з UB, а не просто закінчити в кінці найкоротшого діапазону в групі?
Catskul

18

Тому я писав цей zip раніше, коли мені стало нудно, я вирішив його опублікувати, оскільки він відрізняється від інших тим, що він не використовує boost і більше схожий на c ++ stdlib.

template <typename Iterator>
    void advance_all (Iterator & iterator) {
        ++iterator;
    }
template <typename Iterator, typename ... Iterators>
    void advance_all (Iterator & iterator, Iterators& ... iterators) {
        ++iterator;
        advance_all(iterators...);
    } 
template <typename Function, typename Iterator, typename ... Iterators>
    Function zip (Function func, Iterator begin, 
            Iterator end, 
            Iterators ... iterators)
    {
        for(;begin != end; ++begin, advance_all(iterators...))
            func(*begin, *(iterators)... );
        //could also make this a tuple
        return func;
    }

Приклад використання:

int main () {
    std::vector<int> v1{1,2,3};
    std::vector<int> v2{3,2,1};
    std::vector<float> v3{1.2,2.4,9.0};
    std::vector<float> v4{1.2,2.4,9.0};
     zip (
            [](int i,int j,float k,float l){
                std::cout << i << " " << j << " " << k << " " << l << std::endl;
            },
            v1.begin(),v1.end(),v2.begin(),v3.begin(),v4.begin());
}

4
Вам слід перевірити, чи є в кінці будь-який з ітераторів.
Xeo

1
@Xeo всі діапазони повинні мати такий самий розмір, як перший або більший
aaronman

Ви можете пояснити, як це [](int i,int j,float k,float l)працює? Це лямбда-функція?
Підключено

@Hooked так, це лямбда, це в основному працює просто, std::for_eachале ви можете використовувати довільну кількість діапазонів, параметри в лямбда залежать від того, скільки ітераторів ви надаєте функції
aaronman

1
Типовою потребою є стискання діапазонів різного розміру або навіть з нескінченними діапазонами.
Xeo

16

Ви можете використовувати рішення на основі boost::zip_iterator. Створіть клас фальшивих контейнерів, підтримуючи посилання на ваші контейнери, і які повертаються zip_iteratorз функцій beginand і endmember. Тепер ти можеш писати

for (auto p: zip(c1, c2)) { ... }

Приклад реалізації (будь ласка, протестуйте):

#include <iterator>
#include <boost/iterator/zip_iterator.hpp>

template <typename C1, typename C2>
class zip_container
{
    C1* c1; C2* c2;

    typedef boost::tuple<
        decltype(std::begin(*c1)), 
        decltype(std::begin(*c2))
    > tuple;

public:
    zip_container(C1& c1, C2& c2) : c1(&c1), c2(&c2) {}

    typedef boost::zip_iterator<tuple> iterator;

    iterator begin() const
    {
         return iterator(std::begin(*c1), std::begin(*c2));
    }

    iterator end() const
    {
         return iterator(std::end(*c1), std::end(*c2));
    }
};

template <typename C1, typename C2>
zip_container<C1, C2> zip(C1& c1, C2& c2)
{
    return zip_container<C1, C2>(c1, c2);
}

Варіадичну версію я залишаю читачеві як чудову вправу.


3
+1: Boost.Range, ймовірно, повинен це враховувати. Насправді я відкину їм запит на можливість щодо цього.
Nicol Bolas

2
@NicolBolas: Ти добре робиш. Це має бути досить легко здійснити за допомогою boost::iterator_range+ boost::zip_iterator, навіть варіадичної версії.
Alexandre C.

1
Я вважаю, що це ніколи не припиниться (і матиме невизначену поведінку), якщо діапазони не однакової довжини.
Джонатан Уейклі

1
boost::zip_iteratorне працює з діапазонами різної довжини
Джонатан Уейклі

1
Це також повинно працювати навіть у чистому c ++ 03 з парою замість кортежу. Однак це також створює проблеми, коли довжини не рівні. Щось можна зробити з кінцем (), взявши відповідний кінець () найменшого контейнера. Здається, це в специфікації, як це було у питанні ОП.
Пол

16

std :: transform може зробити це тривіально:

std::vector<int> a = {1,2,3,4,5};
std::vector<int> b = {1,2,3,4,5};
std::vector<int>c;
std::transform(a.begin(),a.end(), b.begin(),
               std::back_inserter(c),
               [](const auto& aa, const auto& bb)
               {
                   return aa*bb;
               });
for(auto cc:c)
    std::cout<<cc<<std::endl;

Якщо друга послідовність коротша, здається, що моя реалізація надає значення за замовчуванням.


1
Якщо 2-а послідовність коротша, то я б очікував, що це UB, як ви б ітерацію закінчили до кінця b.
Адріан

15

Зверніться <redi/zip.h>до zipфункції, яка працює з діапазоном-базою forі приймає будь-яку кількість діапазонів, які можуть бути rvalues ​​або lvalues ​​і можуть бути різної довжини (ітерація зупиниться в кінці найкоротшого діапазону).

std::vector<int> vi{ 0, 2, 4 };
std::vector<std::string> vs{ "1", "3", "5", "7" };
for (auto i : redi::zip(vi, vs))
  std::cout << i.get<0>() << ' ' << i.get<1>() << ' ';

Друкує 0 1 2 3 4 5


2
Ви можете також використовувати boost/tuple/tuple_io.hppдляcout << i;
kirill_igum

Це те, що мені вдалося. Однак у своєму коді мені довелося використовувати еквівалент boost::get<0>(i)і boost::get<1>(i). Я не впевнений, чому оригінальний зразок не може бути адаптований безпосередньо, можливо, це пов'язано з тим, що мій код постійно посилається на контейнери.
YitzikC

11

З діапазоном-v3 :

#include <range/v3/all.hpp>
#include <vector>
#include <iostream>

namespace ranges {
    template <class T, class U>
    std::ostream& operator << (std::ostream& os, common_pair<T, U> const& p)
    {
      return os << '(' << p.first << ", " << p.second << ')';
    }
}

using namespace ranges::v3;

int main()
{
    std::vector<int> a {4, 5, 6};
    double b[] = {7, 8, 9};
    std::cout << view::zip(a, b) << std::endl; 
}

Вихід:

[(4, 7), (5, 8), (6, 9)]


@ einpoklum-reinstateMonica зараз!
yuyoyuppe

6

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

https://github.com/cshelton/zipfor

Наприклад, ви можете це зробити

vector<int> a {1,2,3};
array<string,3> b {"hello","there","coders"};

zipfor(i,s eachin a,b)
    cout << i << " => " << s << endl;

Головний синтаксичний цукор полягає в тому, що я можу називати елементи з кожного контейнера. Я також включаю "mapfor", який робить те ж саме, але для карт (щоб назвати ".перший" та ".second" елемента).


Це акуратно! Чи можна взяти довільну кількість аргументів - це всі аргументи, обмежені вашим розумним шаблоном до кінцевого числа?
Підключений

В даний час він обробляє лише до 9 паралельних контейнерів. Це було б просто заздалегідь. У той час як варіативні макроси дозволяють одному макросу "zipfor" обробляти різну кількість параметрів, все одно потрібно кодувати окремий макрос для кожного (який буде відправлено). Див groups.google.com/forum/?fromgroups = #! Topic / comp.std.c / ... і stackoverflow.com/questions/15847837 / ...
cshelton

Чи добре він обробляє аргументи різного розміру? (як описано в OP)
coyotte508

@ coyotte508, він припускає, що перший контейнер має найменшу кількість елементів (і ігнорує зайві елементи в інших контейнерах). Було б легко модифікувати, щоб не робити цього припущення, але це сповільнювало б його (в даний час це не повільніше, ніж від руки), коли кількість елементів збігається.
cshelton


6

Якщо вам подобається перевантаження оператора, ось три можливості. Перші два використовують std::pair<>і std::tuple<>, відповідно, як ітератори; третій поширює це на діапазон for. Зверніть увагу, що ці визначення операторів сподобаються не всім, тому найкраще зберігати їх в окремому просторі імен і мати using namespaceфункцію (а не файли!), Де ви хочете їх використовувати.

#include <iostream>
#include <utility>
#include <vector>
#include <tuple>

// put these in namespaces so we don't pollute global
namespace pair_iterators
{
    template<typename T1, typename T2>
    std::pair<T1, T2> operator++(std::pair<T1, T2>& it)
    {
        ++it.first;
        ++it.second;
        return it;
    }
}

namespace tuple_iterators
{
    // you might want to make this generic (via param pack)
    template<typename T1, typename T2, typename T3>
    auto operator++(std::tuple<T1, T2, T3>& it)
    {
        ++( std::get<0>( it ) );
        ++( std::get<1>( it ) );
        ++( std::get<2>( it ) );
        return it;
    }

    template<typename T1, typename T2, typename T3>
    auto operator*(const std::tuple<T1, T2, T3>& it)
    {
        return std::tie( *( std::get<0>( it ) ),
                         *( std::get<1>( it ) ),
                         *( std::get<2>( it ) ) );
    }

    // needed due to ADL-only lookup
    template<typename... Args>
    struct tuple_c
    {
        std::tuple<Args...> containers;
    };

    template<typename... Args>
    auto tie_c( const Args&... args )
    {
        tuple_c<Args...> ret = { std::tie(args...) };
        return ret;
    }

    template<typename T1, typename T2, typename T3>
    auto begin( const tuple_c<T1, T2, T3>& c )
    {
        return std::make_tuple( std::get<0>( c.containers ).begin(),
                                std::get<1>( c.containers ).begin(),
                                std::get<2>( c.containers ).begin() );
    }

    template<typename T1, typename T2, typename T3>
    auto end( const tuple_c<T1, T2, T3>& c )
    {
        return std::make_tuple( std::get<0>( c.containers ).end(),
                                std::get<1>( c.containers ).end(),
                                std::get<2>( c.containers ).end() );
    }

    // implement cbegin(), cend() as needed
}

int main()
{
    using namespace pair_iterators;
    using namespace tuple_iterators;

    std::vector<double> ds = { 0.0, 0.1, 0.2 };
    std::vector<int   > is = {   1,   2,   3 };
    std::vector<char  > cs = { 'a', 'b', 'c' };

    // classical, iterator-style using pairs
    for( auto its  = std::make_pair(ds.begin(), is.begin()),
              end  = std::make_pair(ds.end(),   is.end()  ); its != end; ++its )
    {
        std::cout << "1. " << *(its.first ) + *(its.second) << " " << std::endl;
    }

    // classical, iterator-style using tuples
    for( auto its  = std::make_tuple(ds.begin(), is.begin(), cs.begin()),
              end  = std::make_tuple(ds.end(),   is.end(),   cs.end()  ); its != end; ++its )
    {
        std::cout << "2. " << *(std::get<0>(its)) + *(std::get<1>(its)) << " "
                           << *(std::get<2>(its)) << " " << std::endl;
    }

    // range for using tuples
    for( const auto& d_i_c : tie_c( ds, is, cs ) )
    {
        std::cout << "3. " << std::get<0>(d_i_c) + std::get<1>(d_i_c) << " "
                           << std::get<2>(d_i_c) << " " << std::endl;
    }
}

3

Для бібліотеки обробки потоків C ++, яку я пишу, я шукав рішення, яке не покладається на сторонні бібліотеки і працює з довільною кількістю контейнерів. Я закінчив із цим рішенням. Це схоже на прийняте рішення, яке використовує boost (а також призводить до невизначеної поведінки, якщо довжини контейнера не рівні)

#include <utility>

namespace impl {

template <typename Iter, typename... Iters>
class zip_iterator {
public:
  using value_type = std::tuple<const typename Iter::value_type&,
                                const typename Iters::value_type&...>;

  zip_iterator(const Iter &head, const Iters&... tail)
      : head_(head), tail_(tail...) { }

  value_type operator*() const {
    return std::tuple_cat(std::tuple<const typename Iter::value_type&>(*head_), *tail_);
  }

  zip_iterator& operator++() {
    ++head_; ++tail_;
    return *this;
  }

  bool operator==(const zip_iterator &rhs) const {
    return head_ == rhs.head_ && tail_ == rhs.tail_;
  }

  bool operator!=(const zip_iterator &rhs) const {
    return !(*this == rhs);
  }

private:
  Iter head_;
  zip_iterator<Iters...> tail_;
};

template <typename Iter>
class zip_iterator<Iter> {
public:
  using value_type = std::tuple<const typename Iter::value_type&>;

  zip_iterator(const Iter &head) : head_(head) { }

  value_type operator*() const {
    return value_type(*head_);
  }

  zip_iterator& operator++() { ++head_; return *this; }

  bool operator==(const zip_iterator &rhs) const { return head_ == rhs.head_; }

  bool operator!=(const zip_iterator &rhs) const { return !(*this == rhs); }

private:
  Iter head_;
};

}  // namespace impl

template <typename Iter>
class seq {
public:
  using iterator = Iter;
  seq(const Iter &begin, const Iter &end) : begin_(begin), end_(end) { }
  iterator begin() const { return begin_; }
  iterator end() const { return end_; }
private:
  Iter begin_, end_;
};

/* WARNING: Undefined behavior if iterator lengths are different.
 */
template <typename... Seqs>
seq<impl::zip_iterator<typename Seqs::iterator...>>
zip(const Seqs&... seqs) {
  return seq<impl::zip_iterator<typename Seqs::iterator...>>(
      impl::zip_iterator<typename Seqs::iterator...>(std::begin(seqs)...),
      impl::zip_iterator<typename Seqs::iterator...>(std::end(seqs)...));
}

1
посилання зламане ... було б корисно, якщо пост показує, як ним користуватися, наприклад main ()?
javaLover

@javaLover: ви можете використовувати його так само, як cppitertools у відповіді @ knedlsepp. Одна помітна відмінність полягає в тому, що за допомогою вищезазначеного рішення ви не можете змінювати базові контейнери, оскільки operator*for seq::iteratorповертає std::tupleпосилання const.
Віннету

2

Якщо у вас є компілятор, сумісний із C ++ 14 (наприклад, gcc5), ви можете використовувати zipнаданий у cppitertoolsбібліотеці Райан Хейнінг, що виглядає дуже перспективно:

array<int,4> i{{1,2,3,4}};
vector<float> f{1.2,1.4,12.3,4.5,9.9};
vector<string> s{"i","like","apples","alot","dude"};
array<double,5> d{{1.2,1.2,1.2,1.2,1.2}};

for (auto&& e : zip(i,f,s,d)) {
    cout << std::get<0>(e) << ' '
         << std::get<1>(e) << ' '
         << std::get<2>(e) << ' '
         << std::get<3>(e) << '\n';
    std::get<1>(e)=2.2f; // modifies the underlying 'f' array
}

0

Boost.Iterators zip_iteratorможна використовувати (приклади в документації). Це не буде працювати з діапазоном для, але ви можете використовувати std::for_eachі лямбду.


Чому це не буде працювати з діапазоном для? Поєднайте його з Boost.Range, і ви повинні налаштуватися.
Xeo

@Xeo: Я не дуже добре знаю Діапазон. Думаю, ви могли б залучити якийсь шаблонний шаблон і змусити це працювати, але просто використання IMO for_eachбуло б менш клопотом.
Cat Plus Plus

Ви маєте на увазі, що щось подібне не є клопотом std::for_each(make_zip_iterator(make_tuple(Y1.begin(), Y2.begin())), make_zip_iterator(make_tuple(Y1.end(), Y2.end())), [](const tuple<int, int>& t){printf("%d %d\n", get<0>(t), get<1>(t)); });:?
UncleBens

2
Мені слід розпочати кампанію Lambda Does Not Make std :: for_each Корисною. :)
UncleBens

2
@Xeo: Це, мабуть, має бути окреме питання, але чому ж ??
UncleBens

-2

Ось проста версія, яка не вимагає посилення. Це не буде особливо ефективно, оскільки створює тимчасові значення і не узагальнює над контейнерами, крім списків, але не має залежностей і вирішує найпоширеніший випадок архівування.

template<class L, class R>
std::list< std::pair<L,R> >  zip(std::list<L> left, std::list<R> right)
{
auto l = left.begin();
auto r = right.begin();
std::list< std::pair<L,R> > result;
  while( l!=left.end() && r!=right.end() )
    result.push_back( std::pair<L,R>( *(l++), *(r++) ) );
  return result;
}

Хоча інші версії є більш гнучкими, часто сенс використання оператора списку полягає у створенні простого однокласного вкладиша. Ця версія має перевагу в тому, що загальний випадок простий.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.