Czyste sposoby pisania wielu pętli „for”

98

W przypadku tablicy o wielu wymiarach zwykle musimy napisać forpętlę dla każdego z jej wymiarów. Na przykład:

vector< vector< vector<int> > > A;

for (int k=0; k<A.size(); k++)
{
    for (int i=0; i<A[k].size(); i++)
    {
        for (int j=0; j<A[k][i].size(); j++)
        {
            do_something_on_A(A[k][i][j]);
        }
    }
}

double B[10][8][5];
for (int k=0; k<10; k++)
{
    for (int i=0; i<8; i++)
    {
        for (int j=0; j<5; j++)
        {
            do_something_on_B(B[k][i][j]);
        }
    }
}

for-for-forCzęsto widzisz tego rodzaju pętle w naszym kodzie. Jak używać makr do definiowania for-for-forpętli, aby nie musieć za każdym razem ponownie pisać tego rodzaju kodu? Czy jest lepszy sposób na zrobienie tego?

C. Wang
źródło
62
Oczywistą odpowiedzią jest to, że nie. Nie tworzysz nowego języka za pomocą makr (ani żadnej innej techniki); osoba, która przyjdzie po tobie, nie będzie w stanie odczytać kodu.
James Kanze
17
Kiedy masz wektor wektora wektora, jest to oznaka złego projektu.
Maroun,
5
@Nim: Możesz to zrobić z 1 płaską tablicą (nie jestem pewien, czy jest lepsza).
Jarod42
16
Myślę, że nie chcesz ukrywać potencjalnego O(n) = n^3kodu ...
poy
36
@ TC1: I wtedy będzie mi trudniej czytać. To kwestia osobistych preferencji i tak naprawdę nie pomaga w przypadku tego pytania.
ereOn

Odpowiedzi:

281

Po pierwsze, nie używasz takiej struktury danych. Jeśli potrzebujesz trójwymiarowej macierzy, możesz ją zdefiniować:

class Matrix3D
{
    int x;
    int y;
    int z;
    std::vector<int> myData;
public:
    //  ...
    int& operator()( int i, int j, int k )
    {
        return myData[ ((i * y) + j) * z + k ];
    }
};

Lub jeśli chcesz indeksować używając [][][], potrzebujesz, operator[] który zwraca proxy.

Gdy już to zrobisz, jeśli okaże się, że ciągle musisz iterować tak, jak przedstawiłeś, udostępniasz iterator, który będzie to obsługiwał:

class Matrix3D
{
    //  as above...
    typedef std::vector<int>::iterator iterator;
    iterator begin() { return myData.begin(); }
    iterator end()   { return myData.end();   }
};

Następnie po prostu piszesz:

for ( Matrix3D::iterator iter = m.begin(); iter != m.end(); ++ iter ) {
    //  ...
}

(Lub tylko:

for ( auto& elem: m ) {
}

jeśli masz C ++ 11.)

A jeśli potrzebujesz trzech indeksów podczas takich iteracji, możesz stworzyć iterator, który je ujawni:

class Matrix3D
{
    //  ...
    class iterator : private std::vector<int>::iterator
    {
        Matrix3D const* owner;
    public:
        iterator( Matrix3D const* owner,
                  std::vector<int>::iterator iter )
            : std::vector<int>::iterator( iter )
            , owner( owner )
        {
        }
        using std::vector<int>::iterator::operator++;
        //  and so on for all of the iterator operations...
        int i() const
        {
            ((*this) -  owner->myData.begin()) / (owner->y * owner->z);
        }
        //  ...
    };
};
James Kanze
źródło
21
Ta odpowiedź powinna być o wiele bardziej przychylna, ponieważ jest jedyną, która dotyczy rzeczywistego źródła problemu.
ereOn
5
to może być poprawna odpowiedź, ale nie zgadzam się, że jest dobra. wiele tajemniczych kodów szablonów z prawdopodobnie 10-krotnie wolnym czasem kompilacji i prawdopodobnie 10-krotnie wolnym kodem debugowania (może być więcej). Dla mnie zdecydowanie oryginalny kod jest dla mnie o wiele bardziej przejrzysty ...
Gorkem
10
@beehorf ... a także dużo, dużo wolniej. Ponieważ tablice wielowymiarowe w C i C ++ są w rzeczywistości tablicami zagnieżdżonymi w tym sensie, że wymiary zewnętrzne przechowują wskaźniki do tablic zagnieżdżonych. Te zagnieżdżone tablice są następnie arbitralnie rozrzucane w pamięci, skutecznie eliminując wszelkie pobieranie wstępne i buforowanie. Znam przykłady, w których ktoś napisał kod, używając vector<vector<vector<double> > >do reprezentowania trójwymiarowego pola. Przepisanie kodu na odpowiednik powyższego rozwiązania spowodowało przyspieszenie o 10.
Michael Wild
5
@beehorf Gdzie widzisz kod szablonu? (W praktyce Matrix3Dprawdopodobnie powinien to być szablon, ale jest to bardzo prosty szablon). I musisz tylko debugować Matrix3D, nie za każdym razem, gdy potrzebujesz macierzy 3D, więc oszczędzasz ogromną ilość czasu na debugowaniu. Jeśli chodzi o jasność: jak jest std::vector<std::vector<std::vector<int>>>jaśniejsza niż Matrix3D? Nie wspominając o tym, że Matrix3Dwymusza to fakt, że masz macierz, podczas gdy zagnieżdżone wektory mogą być postrzępione, a powyższe jest prawdopodobnie znacznie szybsze.
James Kanze,
10
@MichaelWild Ale oczywiście prawdziwą zaletą mojego podejścia jest to, że możesz zmienić reprezentację, w zależności od tego, co jest szybsze w twoim środowisku, bez konieczności modyfikowania kodu klienta. Kluczem do dobrej wydajności jest odpowiednia hermetyzacja, dzięki czemu można wprowadzać zmiany, o których profiler mówi, że są potrzebne, bez konieczności przepisywania całej aplikacji.
James Kanze,
44

Używanie makra do ukrycia forpętli może być bardzo mylące, wystarczy zaoszczędzić kilka znaków. Użyłbym piecyk do pętli zamiast:

for (auto& k : A)
    for (auto& i : k)
        for (auto& j : i)
            do_something_on_A(j);

Oczywiście można zastąpić auto&ze const auto&jeśli jesteś w rzeczywistości, nie modyfikując dane.

But
źródło
3
Zakładając, że OP może używać C ++ 11.
Jarod42,
1
@herohuyongtao W przypadku iteratorów. Co może być bardziej idiomatyczne w tym przypadku, ale są przypadki, w których potrzebujesz trzech intzmiennych.
James Kanze
1
A nie powinno tak być do_something_on_A(*j)?
James Kanze
1
@Jefffrey Ah, tak. Kolejny powód, by przeliterować typ. (Wydaje mi się, że użycie autofor ki imoże być uzasadnione. Tyle że nadal rozwiązuje problem na niewłaściwym poziomie; prawdziwy problem polega na tym, że używa on wektorów zagnieżdżonych.)
James Kanze
2
@Dhara kto cały wektor wektorów (cóż, odniesienie do niego), a nie indeks.
Yakk - Adam Nevraumont
21

Coś takiego może pomóc:

 template <typename Container, typename Function>
 void for_each3d(const Container &container, Function function)
 {
     for (const auto &i: container)
         for (const auto &j: i)
             for (const auto &k: j)
                 function(k);
 }

 int main()
 {
     vector< vector< vector<int> > > A;     
     for_each3d(A, [](int i){ std::cout << i << std::endl; });

     double B[10][8][5] = { /* ... */ };
     for_each3d(B, [](double i){ std::cout << i << std::endl; });
 }

Aby było N-ary, potrzebujemy trochę magii szablonów. Przede wszystkim powinniśmy stworzyć strukturę SFINAE, aby rozróżnić, czy ta wartość czy kontener. Domyślna implementacja wartości i specjalizacji dla tablic i każdego z typów kontenerów. Jak zauważa @Zeta, możemy określić standardowe kontenery po zagnieżdżonym iteratortypie (najlepiej powinniśmy sprawdzić, czy typ może być używany z podstawą zakresu, forczy nie).

 template <typename T>
 struct has_iterator
 {
     template <typename C>
     constexpr static std::true_type test(typename C::iterator *);

     template <typename>
     constexpr static std::false_type test(...);

     constexpr static bool value = std::is_same<
         std::true_type, decltype(test<typename std::remove_reference<T>::type>(0))
     >::value;
 };

 template <typename T>
 struct is_container : has_iterator<T> {};

 template <typename T>
 struct is_container<T[]> : std::true_type {};

 template <typename T, std::size_t N>
 struct is_container<T[N]> : std::true_type {}; 

 template <class... Args>
 struct is_container<std::vector<Args...>> : std::true_type {};

Wdrożenie for_eachjest proste. Domyślna funkcja wywoła function:

 template <typename Value, typename Function>
 typename std::enable_if<!is_container<Value>::value, void>::type
 rfor_each(const Value &value, Function function)
 {
     function(value);
 }

A specjalizacja będzie się nazywać rekurencyjnie:

 template <typename Container, typename Function>
 typename std::enable_if<is_container<Container>::value, void>::type
 rfor_each(const Container &container, Function function)
 {
     for (const auto &i: container)
         rfor_each(i, function);
 }

I voila:

 int main()
 {
     using namespace std;
     vector< vector< vector<int> > > A;
     A.resize(3, vector<vector<int> >(3, vector<int>(3, 5)));
     rfor_each(A, [](int i){ std::cout << i << ", "; });
     // 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,

     std::cout << std::endl;
     double B[3][3] = { { 1. } };
     rfor_each(B, [](double i){ std::cout << i << ", "; });
     // 1, 0, 0, 0, 0, 0, 0, 0, 0,
 }

Również to nie zadziała dla wskaźników (tablice przydzielone w stercie).

fasked
źródło
@herohuyongtao z ograniczeniami możemy zaimplementować dwie specjalizacje dla Containeri dla innych.
fasked
1
@herohuyongtao Zrobiłem przykład K-ary foreach.
fasked
1
@fasked: Użyj is_container : has_iterator<T>::valuemojej odpowiedzi i nie musisz pisać specjalizacji dla każdego typu, ponieważ każdy kontener powinien mieć iteratortypedef. Zapraszam do całkowitego wykorzystania czegokolwiek z mojej odpowiedzi, twoja jest już lepsza.
Zeta,
@Zeta +1 za to. ContainerPomoże też, jak wspomniałem, koncepcja.
fasked
::iteratornie tworzy iterowalnego zakresu. int x[2][3][4]jest doskonale iterowalny, ponieważ struct foo { int x[3]; int* begin() { return x; } int* end() { return x+3; } }; nie jestem pewien, jaką T[]specjalizację ma robić?
Yakk - Adam Nevraumont
17

Większość odpowiedzi po prostu pokazuje, jak C ++ można przekształcić w niezrozumiałe rozszerzenia składniowe, IMHO.

Definiując jakiekolwiek szablony lub makra, po prostu zmusisz innych programistów do zrozumienia fragmentów zaciemnionego kodu zaprojektowanego w celu ukrycia innych fragmentów zaciemnionego kodu.
Zmusisz każdego gościa, który czyta Twój kod, do posiadania wiedzy na temat szablonów, aby uniknąć wykonywania swojej pracy polegającej na definiowaniu obiektów z jasną semantyką.

Jeśli zdecydowałeś się użyć surowych danych, takich jak trójwymiarowe tablice, po prostu żyj z nimi lub zdefiniuj klasę, która nadaje zrozumiałe znaczenie Twoim danym.

for (auto& k : A)
for (auto& i : k)
for (auto& current_A : i)
    do_something_on_A(current_A);

jest po prostu zgodny z tajemniczą definicją wektora wektora wektora int bez wyraźnej semantyki.

kuroi neko
źródło
10
#include "stdio.h"

#define FOR(i, from, to)    for(int i = from; i < to; ++i)
#define TRIPLE_FOR(i, j, k, i_from, i_to, j_from, j_to, k_from, k_to)   FOR(i, i_from, i_to) FOR(j, j_from, j_to) FOR(k, k_from, k_to)

int main()
{
    TRIPLE_FOR(i, j, k, 0, 3, 0, 4, 0, 2)
    {
        printf("i: %d, j: %d, k: %d\n", i, j, k);
    }
    return 0;
}

AKTUALIZACJA: Wiem, że o to prosiłeś, ale lepiej tego nie używaj :)

FreeNickname
źródło
5
Wiem, że o to prosił OP, ale poważnie ... Wygląda to na wspaniały przykład zaciemniania. Przypuśćmy, że TRIPLE_FORzostały zdefiniowane w jakimś nagłówku, co mam myśleć, gdy widzę tutaj `TRIPLE_FOR.
James Kanze
2
Tak, chyba masz rację :) Myślę, że zostawię to tutaj tylko jako przykład, że można to zrobić za pomocą makr, ale dodaj notatkę, że lepiej tego nie robić :) Właśnie się obudziłem i postanowiłem użyć tego pytania jako małej rozgrzewki dla umysłu.
FreeNickname
5

Jednym z pomysłów jest napisanie iterowalnej klasy pseudokontenerowej, która „zawiera” zestaw wszystkich krotek z wieloma indeksami, które będą indeksowane. Brak implementacji, ponieważ zajmie to zbyt dużo czasu, ale pomysł jest taki, że powinieneś być w stanie napisać ...

multi_index mi (10, 8, 5);
  //  The pseudo-container whose iterators give {0,0,0}, {0,0,1}, ...

for (auto i : mi)
{
  //  In here, use i[0], i[1] and i[2] to access the three index values.
}
Steve314
źródło
najlepsza odpowiedź tutaj imo.
davidhigh
4

Widzę tutaj wiele odpowiedzi, które działają rekurencyjnie, wykrywając, czy dane wejściowe są kontenerem, czy nie. Zamiast tego, dlaczego nie wykryć, czy bieżąca warstwa jest tego samego typu, co funkcja? Jest znacznie prostszy i pozwala na bardziej zaawansowane funkcje:

//This is roughly what we want for values
template<class input_type, class func_type> 
void rfor_each(input_type&& input, func_type&& func) 
{ func(input);}

//This is roughly what we want for containers
template<class input_type, class func_type>
void rfor_each(input_type&& input, func_type&& func) 
{ for(auto&& i : input) rfor_each(i, func);}

Jednak to (oczywiście) daje nam niejednoznaczne błędy. Więc używamy SFINAE do wykrywania, czy bieżące wejście pasuje do funkcji, czy nie

//Compiler knows to only use this if it can pass input to func
template<class input_type, class func_type>
auto rfor_each(input_type&& input, func_type&& func) ->decltype(func(input)) 
{ return func(input);}

//Otherwise, it always uses this one
template<class input_type, class func_type>
void rfor_each(input_type&& input, func_type&& func) 
{ for(auto&& i : input) rfor_each(i, func);}

To teraz poprawnie obsługuje kontenery, ale kompilator nadal uważa to za niejednoznaczne dla input_types, które można przekazać do funkcji. Więc używamy standardowej sztuczki C ++ 03, aby preferować pierwszą funkcję od drugiej, również przekazując zero i sprawiając, że ta, którą wolimy, accept i int, a druga zajmie ...

template<class input_type, class func_type>
auto rfor_each(input_type&& input, func_type&& func, int) ->decltype(func(input)) 
{ return func(input);}

//passing the zero causes it to look for a function that takes an int
//and only uses ... if it absolutely has to 
template<class input_type, class func_type>
void rfor_each(input_type&& input, func_type&& func, ...) 
{ for(auto&& i : input) rfor_each(i, func, 0);}

Otóż ​​to. Sześć stosunkowo prostych wierszy kodu i możesz iterować po wartościach, wierszach lub dowolnej innej podjednostce, w przeciwieństwie do wszystkich innych odpowiedzi.

#include <iostream>
int main()
 {

     std::cout << std::endl;
     double B[3][3] = { { 1.2 } };
     rfor_each(B[1], [](double&v){v = 5;}); //iterate over doubles
     auto write = [](double (&i)[3]) //iterate over rows
         {
             std::cout << "{";
             for(double d : i) 
                 std::cout << d << ", ";
             std::cout << "}\n";
         };
     rfor_each(B, write );
 };

Dowód kompilacji i wykonania tu i tutaj

Jeśli chcesz wygodniejszej składni w C ++ 11, możesz dodać makro. (Obserwowanie nie zostało przetestowane)

template<class container>
struct container_unroller {
    container& c;
    container_unroller(container& c_) :c(c_) {}
    template<class lambda>
    void operator <=(lambda&& l) {rfor_each(c, l);}
};
#define FOR_NESTED(type, index, container) container_unroller(container) <= [](type& index) 
//note that this can't handle functions, function pointers, raw arrays, or other complex bits

int main() {
     double B[3][3] = { { 1.2 } };
     FOR_NESTED(double, v, B) {
         std::cout << v << ", ";
     }
}
Mooing Duck
źródło
3

Zastrzegam tę odpowiedź następującym stwierdzeniem: zadziałałoby to tylko wtedy, gdybyś działał na rzeczywistej tablicy - nie zadziała w twoim przykładzie std::vector.

Jeśli wykonujesz tę samą operację na każdym elemencie wielowymiarowej tablicy, nie dbając o położenie każdego elementu, możesz skorzystać z faktu, że tablice są umieszczane w ciągłych lokalizacjach pamięci i traktować całość jako jedną duża jednowymiarowa tablica. Na przykład, jeśli w drugim przykładzie chcielibyśmy pomnożyć każdy element przez 2,0:

double B[3][3][3];
// ... set the values somehow
double* begin = &B[0][0][0];     // get a pointer to the first element
double* const end = &B[3][0][0]; // get a (const) pointer past the last element
for (; end > begin; ++begin) {
    (*begin) *= 2.0;
}

Zwróć uwagę, że powyższe podejście pozwala również na użycie pewnych „właściwych” technik C ++:

double do_something(double d) {
    return d * 2.0;
}

...

double B[3][3][3];
// ... set the values somehow
double* begin = &B[0][0][0];  // get a pointer to the first element
double* end = &B[3][0][0];    // get a pointer past the last element

std::transform(begin, end, begin, do_something);

I zazwyczaj nie radzę tego podejścia (preferując coś podobnego odpowiedź Jefffrey'S), gdyż opiera się na zdefiniowaniu rozmiary dla tablic, ale w niektórych przypadkach może to być przydatne.

icabod
źródło
To jest nielegalne: stackoverflow.com/questions/6015080/ ...
ecatmur
@ecatmur: Interesujące - dopiero co zabrałem się do pracy, więc sprawdzę to i odpowiednio zaktualizuję / skasuję odpowiedź. Dzięki.
icabod,
@ecatmur: Spojrzałem na standard C ++ 11 (sekcja 8.3.4) i to, co napisałem, powinno działać i nie wygląda na nielegalne (dla mnie). Podane łącze odnosi się do uzyskiwania dostępu do elementów spoza zdefiniowanej wielkości tablicy. Chociaż prawdą jest, że otrzymuję adres tuż za tablicą, to nie jest to dostęp do danych - ma to na celu zapewnienie „końca”, w taki sam sposób, w jaki można używać wskaźników jako iteratorów, przy czym „koniec” jest jedną minioną ostatni element.
icabod,
Skutecznie uzyskujesz dostęp B[0][0][i]do i >= 3; nie jest to dozwolone, ponieważ uzyskuje dostęp poza (wewnętrzną) tablicą.
ecatmur
1
Jaśniejszy sposób IMO przypisywania końca JEŚLI miałbyś to zrobić to end = start + (xSize * ySize * zSize)
noggin182
2

Byłem trochę zszokowany, że nikt nie zaproponował jakiejś pętli opartej na magii arytmetycznej do wykonania tej pracy. Ponieważ C. Wang szuka rozwiązania bez zagnieżdżonych pętli , zaproponuję jedno:

double B[10][8][5];
int index = 0;

while (index < (10 * 8 * 5))
{
    const int x = index % 10,
              y = (index / 10) % 10,
              z = index / 100;

    do_something_on_B(B[x][y][z]);
    ++index;
}

Cóż, to podejście nie jest eleganckie i elastyczne, więc moglibyśmy spakować cały proces do funkcji szablonu:

template <typename F, typename T, int X, int Y, int Z>
void iterate_all(T (&xyz)[X][Y][Z], F func)
{
    const int limit = X * Y * Z;
    int index = 0;

    while (index < limit)
    {
        const int x = index % X,
                  y = (index / X) % Y,
                  z = index / (X * Y);

        func(xyz[x][y][z]);
        ++index;
    }
}

Tę funkcję szablonu można również wyrazić w postaci zagnieżdżonych pętli:

template <typename F, typename T, int X, int Y, int Z>
void iterate_all(T (&xyz)[X][Y][Z], F func)
{
    for (auto &yz : xyz)
    {
        for (auto &z : yz)
        {
            for (auto &v : z)
            {
                func(v);
            }
        }
    }
}

I można go użyć, dostarczając tablicę 3D o dowolnym rozmiarze oraz nazwę funkcji, pozwalając dedukcji parametrów wykonać ciężką pracę polegającą na zliczaniu rozmiaru każdego wymiaru:

int main()
{
    int A[10][8][5] = {{{0, 1}, {2, 3}}, {{4, 5}, {6, 7}}};
    int B[7][99][8] = {{{0, 1}, {2, 3}}, {{4, 5}, {6, 7}}};

    iterate_all(A, do_something_on_A);
    iterate_all(B, do_something_on_B);

    return 0;
}

W kierunku bardziej ogólnych

Ale po raz kolejny brakuje mu elastyczności, ponieważ działa tylko dla tablic 3D, ale używając SFINAE możemy wykonać pracę dla tablic o dowolnym wymiarze, najpierw potrzebujemy funkcji szablonu, która iteruje tablice rangi 1:

template<typename F, typename A>
typename std::enable_if< std::rank<A>::value == 1 >::type
iterate_all(A &xyz, F func)
{
    for (auto &v : xyz)
    {
        func(v);
    }
}

I jeszcze jeden, który iteruje tablice dowolnej rangi, wykonując rekursję:

template<typename F, typename A>
typename std::enable_if< std::rank<A>::value != 1 >::type
iterate_all(A &xyz, F func)
{
    for (auto &v : xyz)
    {
        iterate_all(v, func);
    }
}

To pozwala nam na iterację wszystkich elementów we wszystkich wymiarach tablicy o dowolnych wymiarach.


Praca z std::vector

W przypadku wielu zagnieżdżonych wektorów rozwiązanie przypomina tablicę o dowolnych wymiarach, ale bez SFINAE: Najpierw będziemy potrzebować funkcji szablonu, która iteruje std::vectorsi i wywołuje żądaną funkcję:

template <typename F, typename T, template<typename, typename> class V>
void iterate_all(V<T, std::allocator<T>> &xyz, F func)
{
    for (auto &v : xyz)
    {
        func(v);
    }
}

I kolejna funkcja szablonu, która iteruje dowolny rodzaj wektorów i nazywa siebie:

template <typename F, typename T, template<typename, typename> class V> 
void iterate_all(V<V<T, std::allocator<T>>, std::allocator<V<T, std::allocator<T>>>> &xyz, F func)
{
    for (auto &v : xyz)
    {
        iterate_all(v, func);
    }
}

Niezależnie od poziomu zagnieżdżenia, iterate_allwywoła wersję wektora wektorów, chyba że wersja wektora wartości jest lepiej dopasowana, kończąc tym samym rekurencję.

int main()
{
    using V0 = std::vector< std::vector< std::vector<int> > >;
    using V1 = std::vector< std::vector< std::vector< std::vector< std::vector<int> > > > >;

    V0 A0 =   {{{0, 1}, {2, 3}}, {{4, 5}, {6, 7}}};
    V1 A1 = {{{{{9, 8}, {7, 6}}, {{5, 4}, {3, 2}}}}};

    iterate_all(A0, do_something_on_A);
    iterate_all(A1, do_something_on_A);

    return 0;
}

Myślę, że treść funkcji jest dość prosta i nieskomplikowana ... Zastanawiam się, czy kompilator mógłby rozwinąć te pętle (jestem prawie pewien, że większość kompilatorów mogłaby rozwinąć pierwszy przykład).

Zobacz demo na żywo tutaj .

Mam nadzieję, że to pomoże.

PaperBirdMaster
źródło
1

Użyj czegoś podobnego do tych linii (pseudokodu, ale idea pozostaje ta sama). Wyodrębniasz wzorzec do zapętlenia raz i za każdym razem stosujesz inną funkcję.

doOn( structure A, operator o)
{
    for (int k=0; k<A.size(); k++)
    {
            for (int i=0; i<A[k].size(); i++)
            {
                for (int j=0; j<A[k][i].size(); j++)
                {
                        o.actOn(A[k][i][j]);
                }
            }
    }
}

doOn(a, function12)
doOn(a, function13)
RobAu
źródło
1

Trzymaj się zagnieżdżonych pętli for!

Wszystkie proponowane tutaj metody mają wady związane z czytelnością lub elastycznością.

Co się stanie, jeśli będziesz musiał wykorzystać wyniki wewnętrznej pętli do przetwarzania w zewnętrznej pętli? Co się stanie, jeśli potrzebujesz wartości z zewnętrznej pętli w wewnętrznej pętli? Większość metod „hermetyzacji” zawodzi tutaj.

Zaufaj mi, widziałem kilka prób „wyczyszczenia” zagnieżdżonych pętli for i ostatecznie okazuje się, że zagnieżdżona pętla jest w rzeczywistości najczystszym i najbardziej elastycznym rozwiązaniem.

James Anderson
źródło
0

Jedną z technik, których użyłem, są szablony. Na przykład:

template<typename T> void do_something_on_A(std::vector<T> &vec) {
    for (auto& i : vec) { // can use a simple for loop in C++03
        do_something_on_A(i);
    }
}

void do_something_on_A(int &val) {
    // this is where your `do_something_on_A` method goes
}

Następnie po prostu wywołujesz do_something_on_A(A)swój główny kod. Funkcja szablonu jest tworzona raz dla każdego wymiaru, pierwszy raz z T = std::vector<std::vector<int>>, drugi raz z T = std::vector<int>.

Możesz uczynić to bardziej ogólnym, używając std::function(lub obiektów podobnych do funkcji w C ++ 03) jako drugiego argumentu, jeśli chcesz:

template<typename T> void do_something_on_vec(std::vector<T> &vec, std::function &func) {
    for (auto& i : vec) { // can use a simple for loop in C++03
        do_something_on_vec(i, func);
    }
}

template<typename T> void do_something_on_vec(T &val, std::function &func) {
    func(val);
}

Następnie nazwij to tak:

do_something_on_vec(A, std::function(do_something_on_A));

Działa to nawet wtedy, gdy funkcje mają tę samą sygnaturę, ponieważ pierwsza funkcja lepiej pasuje do wszystkiego, co zawiera std::vectortyp.

JoshG79
źródło
0

Możesz generować indeksy w jednej pętli w ten sposób (A, B, C to wymiary):

int A = 4, B = 3, C = 3;
for(int i=0; i<A*B*C; ++i)
{
    int a = i/(B*C);
    int b = (i-((B*C)*(i/(B*C))))/C;
    int c = i%C;
}
janek
źródło
Zgadzam się z tobą, jest specjalnie zaprojektowany dla 3 wymiarów;)
janek
1
Nie wspominając o tym, że jest niesamowicie powolny!
noggin182
@ noggin182: pytanie nie dotyczyło szybkości, ale unikania zagnieżdżonych pętli for; poza tym są tam niepotrzebne podziały, i / (B * C) można zastąpić a
janek
Ok, to jest alternatywny sposób, prawdopodobnie bardziej wydajny (javascript): for (var i = 0, j = 0, k = 0; i <A; i + = (j == B-1 && k == C - 1)? 1: 0, j = (k == C - 1)? ((J == B-1)? 0: j + 1): j, k = (k == C - 1)? 0: k + 1) {console.log (i + "" + j + "" + k); }
janek
0

Jedną rzeczą, którą możesz chcieć wypróbować, jeśli masz instrukcje tylko w najbardziej wewnętrznej pętli - a twoim zmartwieniem jest bardziej rozwlekły charakter kodu - jest użycie innego schematu białych znaków. To zadziała tylko wtedy, gdy możesz określić pętle for na tyle zwięźle, aby wszystkie pasowały do ​​jednej linii.

Dla twojego pierwszego przykładu przepisałbym go jako:

vector< vector< vector<int> > > A;
int i,j,k;
for(k=0;k<A.size();k++) for(i=0;i<A[k].size();i++) for(j=0;j<A[k][i].size();j++) {
    do_something_on_A(A[k][i][j]);
}

Jest to trochę popychanie, ponieważ wywołujesz funkcje w zewnętrznych pętlach, co jest równoważne umieszczaniu w nich instrukcji. Usunąłem wszystkie niepotrzebne spacje i może być przejezdne.

Drugi przykład jest znacznie lepszy:

double B[10][8][5];
int i,j,k;

for(k=0;k<10;k++) for(i=0;i<8;i++) for(j=0;j<5;j++) {
    do_something_on_B(B[k][i][j]);
}

Może to być inna konwencja białych znaków, niż lubisz używać, ale daje kompaktowy wynik, który mimo to nie wymaga żadnej wiedzy poza C / C ++ (takiej jak konwencje makr) i nie wymaga żadnych sztuczek, takich jak makra.

Jeśli naprawdę potrzebujesz makra, możesz pójść o krok dalej, wykonując na przykład:

#define FOR3(a,b,c,d,e,f,g,h,i) for(a;b;c) for(d;e;f) for(g;h;i)

co zmieniłoby drugi przykład na:

double B[10][8][5];
int i,j,k;

FOR3(k=0,k<10,k++,i=0,i<8,i++,j=0,j<5,j++) {
    do_something_on_B(B[k][i][j]);
}

i pierwszy przykład też wypada lepiej:

vector< vector< vector<int> > > A;
int i,j,k;
FOR3(k=0,k<A.size(),k++,i=0,i<A[k].size(),i++,j=0,j<A[k][i].size(),j++) {
    do_something_on_A(A[k][i][j]);
}

Miejmy nadzieję, że dość łatwo można powiedzieć, które stwierdzenia pasują do których ze stwierdzeniami. Uważaj też na przecinki, teraz nie możesz ich używać w pojedynczej klauzuli żadnego z fors.

Michael
źródło
1
Czytelność tych jest okropna. Zagłuszanie więcej niż jednej forpętli w wierszu nie czyni go bardziej czytelnym, ale zmniejsza .
0

Oto implementacja C ++ 11, która obsługuje wszystko iterowalne. Inne rozwiązania ograniczają się do kontenerów z ::iteratortypedef lub tablicami: ale a for_eachdotyczy iteracji, a nie kontenera.

Izoluję również SFINAE w jednym miejscu is_iterablecechy. Wysyłanie (między elementami i iterowalnymi) odbywa się za pośrednictwem wysyłania tagów, co moim zdaniem jest jaśniejszym rozwiązaniem.

Kontenery i funkcje zastosowane do elementów są doskonale przekazane, umożliwiając zarówno dostęp do zakresów i funktorów , jak consti brak constdostępu do nich.

#include <utility>
#include <iterator>

Funkcja szablonu, którą implementuję. Wszystko inne może trafić do przestrzeni nazw szczegółów:

template<typename C, typename F>
void for_each_flat( C&& c, F&& f );

Wysyłanie tagów jest znacznie czystsze niż SFINAE. Te dwa są używane odpowiednio dla obiektów iterowalnych i obiektów nieterowalnych. W ostatniej iteracji pierwszej przydałoby się idealne przekazywanie, ale jestem leniwy:

template<typename C, typename F>
void for_each_flat_helper( C&& c, F&& f, std::true_type /*is_iterable*/ ) {
  for( auto&& x : std::forward<C>(c) )
    for_each_flat(std::forward<decltype(x)>(x), f);
}
template<typename D, typename F>
void for_each_flat_helper( D&& data, F&& f, std::false_type /*is_iterable*/ ) {
  std::forward<F>(f)(std::forward<D>(data));
}

To jest szablon wymagany do pisania is_iterable. I zrobić wyszukiwanie argumentów na utrzymaniu begini endw obszarze nazw szczegółów. To naśladuje to, co for( auto x : y )robi pętla dość dobrze:

namespace adl_aux {
  using std::begin; using std::end;
  template<typename C> decltype( begin( std::declval<C>() ) ) adl_begin(C&&);
  template<typename C> decltype( end( std::declval<C>() ) ) adl_end(C&&);
}
using adl_aux::adl_begin;
using adl_aux::adl_end;

TypeSinkJest przydatna do badania, jeśli kod jest poprawny. Robisz TypeSink< decltype(kod ) >i jeśli codejest poprawne, wyrażenie jest void. Jeśli kod jest nieprawidłowy, włącza się SFINAE, a specjalizacja jest blokowana:

template<typename> struct type_sink {typedef void type;};
template<typename T> using TypeSink = typename type_sink<T>::type;

template<typename T, typename=void>
struct is_iterable:std::false_type{};
template<typename T>
struct is_iterable<T, TypeSink< decltype( adl_begin( std::declval<T>() ) ) >>:std::true_type{};

Testuję tylko dla begin. adl_endBadanie może być również wykonywana.

Ostateczna realizacja for_each_flatokazuje się niezwykle prosta:

template<typename C, typename F>
void for_each_flat( C&& c, F&& f ) {
  for_each_flat_helper( std::forward<C>(c), std::forward<F>(f), is_iterable<C>() );
}        

Przykład na żywo

To jest na samym dole: nie krępuj się, aby znaleźć najlepsze odpowiedzi, które są solidne. Chciałem tylko użyć kilku lepszych technik!

Yakk - Adam Nevraumont
źródło
-2

Po pierwsze, nie powinieneś używać wektorów wektorów. Każdy wektor ma zapewnioną ciągłą pamięć, ale „globalna” pamięć wektora wektorów nie jest (i prawdopodobnie nie będzie). Powinieneś także użyć standardowej tablicy typów bibliotek zamiast tablic w stylu C.

using std::array;

array<array<array<double, 5>, 8>, 10> B;
for (int k=0; k<10; k++)
    for (int i=0; i<8; i++)
        for (int j=0; j<5; j++)
            do_something_on_B(B[k][i][j]);

// or, if you really don't like that, at least do this:

for (int k=0; k<10; k++) {
    for (int i=0; i<8; i++) {
        for (int j=0; j<5; j++) {
            do_something_on_B(B[k][i][j]);
        }
    }
}

Jeszcze lepiej, możesz zdefiniować prostą klasę macierzy 3D:

#include <stdexcept>
#include <array>

using std::size_t;

template <size_t M, size_t N, size_t P>
class matrix3d {
    static_assert(M > 0 && N > 0 && P > 0,
                  "Dimensions must be greater than 0.");
    std::array<std::array<std::array<double, P>, N>, M> contents;
public:
    double& at(size_t i, size_t j, size_t k)
    { 
        if (i >= M || j >= N || k >= P)
            throw out_of_range("Index out of range.");
        return contents[i][j][k];
    }
    double& operator(size_t i, size_t j, size_t k)
    {
        return contents[i][j][k];
    }
};

int main()
{
    matrix3d<10, 8, 5> B;
        for (int k=0; k<10; k++)
            for (int i=0; i<8; i++)
                for (int j=0; j<5; j++)
                    do_something_on_B(B(i,j,k));
    return 0;
}

Możesz pójść dalej i uczynić to w pełni poprawnym, dodać mnożenie macierzy (właściwe i elementowe), mnożenie przez wektory itp. Możesz nawet uogólnić to na różne typy (zrobiłbym to szablonem, gdybyś używał głównie podwójnych) .

Możesz także dodać obiekty proxy, aby wykonać B [i] lub B [i] [j]. Mogliby zwrócić wektory (w sensie matematycznym) i macierze pełne podwójnych &, potencjalnie?

Miles Rout
źródło