web-dev-qa-db-fra.com

Combinaison de plusieurs boucles for en un seul itérateur

Dis que j'ai un nid pour la boucle comme 

for (int x = xstart; x < xend; x++){
    for (int y = ystart; y < yend; y++){
        for (int z = zstart; z < zend; z++){
            function_doing_stuff(std::make_Tuple(x, y, z));
        }
    }
}

et voudrait le transformer en

MyRange range(xstart,xend,ystart,yend, zstart,zend);
for (auto point : range){
    function_doing_stuff(point);
}

Comment pourrais-je écrire la classe MyRange pour qu’elle soit aussi efficace que les boucles imbriquées? La motivation de cela est de pouvoir utiliser des algorithmes std (tels que transformer, accumuler, etc.) et de créer un code largement dimensionnel. agnostique.

Avec un itérateur, il serait facile de créer des fonctions basées sur des modèles fonctionnant sur une plage de points 1d, 2d ou 3D. 

La base de code est actuellement C++ 14.

MODIFIER:

Écrire des questions claires est difficile. Je vais essayer de clarifier ... Mon problème n'est pas d'écrire un itérateur, je peux le faire. Au lieu de cela, le problème est celui des performances: est-il possible de créer un itérateur aussi rapide que les boucles for imbriquées?

17
LKlevin

Avec range/v3 , vous pouvez faire

auto xs = ranges::view::iota(xstart, xend);
auto ys = ranges::view::iota(ystart, yend);
auto zs = ranges::view::iota(zstart, zend);
for (const auto& point : ranges::view::cartesian_product(xs, ys, zs)){
    function_doing_stuff(point);
}
6
Jarod42

Une autre option, qui transforme directement le code en boucle, consiste à utiliser un Coroutine . Ceci émule yield à partir de Python ou de C #.

using point = std::Tuple<int, int, int>;
using coro = boost::coroutines::asymmetric_coroutine<point>;

coro::pull_type points(
    [&](coro::Push_type& yield){
        for (int x = xstart; x < xend; x++){
            for (int y = ystart; y < yend; y++){
                for (int z = zstart; z < zend; z++){
                    yield(std::make_Tuple(x, y, z));
                }
            }
        }
    });

for(auto p : points)
    function_doing_stuff(p);
5
Caleth

Vous pouvez introduire votre propre classe en tant que

class myClass {
  public:
    myClass (int x, int y, int z):m_x(x) , m_y(y), m_z(z){};
  private: 
    int m_x, m_y, m_z;

}

puis initialisez un std::vector<myClass> avec votre triple boucle

std::vector<myClass> myVec;
myVec.reserve((xend-xstart)*(yend-ystart)*(zend-zstart)); // alloc memory only once;
for (int x = ystart; x < xend; x++){
    for (int y = xstart; y < yend; y++){ // I assume you have a copy paste error here
        for (int z = zstart; z < zend; z++){
            myVec.Push_back({x,y,z})
        }
    }
}

Enfin, vous pouvez utiliser tous les algorithmes Nice std avec le std::vector<myClass> myVec. Avec le sucre syntaxique 

using MyRange = std::vector<MyClass>;

et

MyRange makeMyRange(int xstart, int xend, int ystart, int yend, int zstart,int zend) {
    MyRange myVec;
    // loop from above
    return MyRange;
}

tu peux écrire 

const MyRange range = makeMyRange(xstart, xend, ystart, yend, zstart, zend);
for (auto point : range){
    function_doing_stuff(point);
}

Avec la nouvelle sémantique de déplacement, cela ne créera pas de copies inutiles. Veuillez noter que l'interface avec cette fonction est plutôt mauvaise. Utilisez peut-être plutôt 3 paires d'int, désignant l'intervalle x, y, z.

Peut-être que vous changez les noms en quelque chose de significatif (par exemple, myClass pourrait être Point). 

5
schorsch312

Voici une implémentation simple qui n’utilise aucune fonctionnalité de langage avancé ni d’autres bibliothèques. La performance devrait être assez proche de la version de la boucle for.

#include <Tuple>

class MyRange {
public:
    typedef std::Tuple<int, int, int> valtype;
    MyRange(int xstart, int xend, int ystart, int yend, int zstart, int zend): xstart(xstart), xend(xend), ystart(ystart), yend(yend), zstart(zstart), zend(zend) {
    }

    class iterator {
    public:
        iterator(MyRange &c): me(c) {
            curvalue = std::make_Tuple(me.xstart, me.ystart, me.zstart);
        }
        iterator(MyRange &c, bool end): me(c) {
            curvalue = std::make_Tuple(end ? me.xend : me.xstart, me.ystart, me.zstart);
        }
        valtype operator*() {
            return curvalue;
        }
        iterator &operator++() {
            if (++std::get<2>(curvalue) == me.zend) {
                std::get<2>(curvalue) = me.zstart;
                if (++std::get<1>(curvalue) == me.yend) {
                    std::get<1>(curvalue) = me.ystart;
                    ++std::get<0>(curvalue);
                }
            }
            return *this;
        }
        bool operator==(const iterator &other) const {
            return curvalue == other.curvalue;
        }
        bool operator!=(const iterator &other) const {
            return curvalue != other.curvalue;
        }
    private:
        MyRange &me;
        valtype curvalue;
    };

    iterator begin() {
        return iterator(*this);
    }

    iterator end() {
        return iterator(*this, true);
    }

private:
    int xstart, xend;
    int ystart, yend;
    int zstart, zend;
};

Et un exemple d'utilisation:

#include <iostream>

void display(std::Tuple<int, int, int> v) {
    std::cout << "(" << std::get<0>(v) << ", " << std::get<1>(v) << ", " << std::get<2>(v) << ")\n";
}

int main() {
    MyRange c(1, 4, 2, 5, 7, 9);
    for (auto v: c) {
        display(v);
    }
}

J'ai laissé des choses comme des itérateurs constants, des operator+= possibles, des décrémentations, des incréments de post, etc. Ils ont été laissés comme un exercice pour le lecteur.

Il stocke les valeurs initiales, puis incrémente chaque valeur à tour de rôle, la rétablissant et incrémentant la suivante lorsqu'elle atteint la valeur finale. C'est un peu comme incrémenter un nombre à plusieurs chiffres.

3
1201ProgramAlarm

En utilisant boost::iterator_facade pour plus de simplicité, vous pouvez épeler tous les membres requis.

Nous avons d'abord une classe qui itère les index N-dimensionnels sous la forme std::array<std::size_t, N>

template<std::size_t N>
class indexes_iterator : public boost::iterator_facade<indexes_iterator, std::array<std::size_t, N>>
{
public:
    template<typename... Dims>
    indexes_iterator(Dims... dims) : dims{ dims... }, values{} {}

private:
    friend class boost::iterator_core_access;

    void increment() { advance(1); }
    void decrement() { advance(-1); }

    void advance(int n) 
    { 
        for (std::size_t i = 0; i < N; ++i)
        { 
            int next = ((values[i] + n) % dims[i]); 
            n = (n \ dims[i]) + (next < value); 
            values[i] = next;
        }
    }

    std::size_t distance(indexes_iterator const & other) const
    {
        std::size_t result = 0, mul = 1;
        for (std::size_t i = 0; i < dims; ++i)
        {
             result += mul * other[i] - values[i];
             mul *= ends[i];
        }
    }

    bool equal(indexes_iterator const& other) const
    {
        return values == other.values;
    }

    std::array<std::size_t, N> & dereference() const { return values; }

    std::array<std::size_t, N> ends;
    std::array<std::size_t, N> values;
}

Nous utilisons ensuite cela pour créer quelque chose de similaire à un boost::Zip_iterator, mais au lieu d'avancer tous ensemble, nous ajoutons nos index.

template <typename... Iterators>
class product_iterator : public boost::iterator_facade<product_iterator<Iterators...>, const std::Tuple<decltype(*std::declval<Iterators>())...>, boost::random_access_traversal_tag>
{
    using ref = std::Tuple<decltype(*std::declval<Iterators>())...>;
public:
    product_iterator(Iterators ... ends) : indexes() , iterators(std::make_Tuple(ends...)) {}
    template <typename ... Sizes>
    product_iterator(Iterators ... begins, Sizes ... sizes) 
      : indexes(sizes...), 
        iterators(begins...) 
    {}
private:
    friend class boost::iterator_core_access;

    template<std::size_t... Is>
    ref dereference_impl(std::index_sequence<Is...> idxs) const
    {
        auto offs = offset(idxs);
        return { *std::get<Is>(offs)... };
    }

    ref dereference() const
    { 
        return dereference_impl(std::index_sequence_for<Iterators...>{}); 
    }

    void increment() { ++indexes; }
    void decrement() { --indexes; }
    void advance(int n) { indexes += n; }

    template<std::size_t... Is>
    std::Tuple<Iterators...> offset(std::index_sequence<Is...>) const
    {
        auto idxs = *indexes;
        return { (std::get<Is>(iterators) + std::get<Is>(idxs))... };
    }

    bool equal(product_iterator const & other) const 
    {
        return offset(std::index_sequence_for<Iterators...>{}) 
            == other.offset(std::index_sequence_for<Iterators...>{}); 
    }

    indexes_iterator<sizeof...(Iterators)> indexes;
    std::Tuple<Iterators...> iterators;
};

Ensuite, nous l'enveloppons dans un boost::iterator_range

template <typename... Ranges>
auto make_product_range(Ranges&&... rngs)
{
    product_iterator<decltype(begin(rngs))...> b(begin(rngs)..., std::distance(std::begin(rngs), std::end(rngs))...);
    product_iterator<decltype(begin(rngs))...> e(end(rngs)...);
    return boost::iterator_range<product_iterator<decltype(begin(rngs))...>>(b, e);
}

int main()
{
    using ranges::view::iota;
    for (auto p : make_product_range(iota(xstart, xend), iota(ystart, yend), iota(zstart, zend)))
        // ...
    return 0;
}

Voir sur godbolt

1
Caleth

Juste une version très simplifiée qui sera aussi efficace qu'une boucle for:

#include <Tuple>

struct iterator{
  int x;
  int x_start;
  int x_end;
  int y;
  int y_start;
  int y_end;
  int z;
  constexpr auto
  operator*() const{
    return std::Tuple{x,y,z};
    }
  constexpr iterator&
  operator++ [[gnu::always_inline]](){
    ++x;
    if (x==x_end){
      x=x_start;
      ++y;
      if (y==y_end) {
        ++z;
        y=y_start;
        }
      }
    return *this;
    }
  constexpr iterator
  operator++(int){
    auto old=*this;
    operator++();
    return old;
    }
  };
struct sentinel{
  int z_end;
  friend constexpr bool
  operator == (const iterator& x,const sentinel& s){
    return x.z==s.z_end;
    }
  friend constexpr bool
  operator == (const sentinel& a,const iterator& x){
    return x==a;
    }
  friend constexpr bool
  operator != (const iterator& x,const sentinel& a){
    return !(x==a);
    }
  friend constexpr bool
  operator != (const sentinel& a,const iterator& x){
    return !(x==a);
    }
  };

struct range{
  iterator start;
  sentinel finish;
  constexpr auto
  begin() const{
    return start;
    }
  constexpr auto
  end()const{
    return finish;
    }
  };

void func(int,int,int);

void test(const range& r){
  for(auto [x,y,z]: r)
    func(x,y,z);
  }
void test(int x_start,int x_end,int y_start,int y_end,int z_start,int z_end){
  for(int z=z_start;z<z_end;++z)
    for(int y=y_start;y<y_end;++y)
      for(int x=x_start;x<x_end;++x)
        func(x,y,z);
  }

L'avantage par rapport à 1201ProgramAlarm answer est le test plus rapide effectué à chaque itération grâce à l'utilisation d'une sentinelle.

0
Oliv