Чисті способи написання декількох циклів "для"


98

Для масиву з декількома розмірами нам зазвичай потрібно написати forцикл для кожного його виміру. Наприклад:

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


62
Очевидна відповідь - ви цього не робите. Ви не створюєте нову мову за допомогою макросів (або будь-якої іншої техніки); людина, яка приходить після вас, не зможе прочитати код.
Джеймс Канзе

17
Коли у вас є вектор вектора, це ознака поганого дизайну.
Maroun

5
@Nim: Ви можете зробити це з 1 плоским масивом (не впевнений, що краще).
Jarod42

16
Я думаю, ви не хочете приховувати потенційний O(n) = n^3код ...
Пой

36
@ TC1: І тоді мені буде важче читати. Це все питання особистих уподобань, і насправді це не допомагає в цьому питанні.
ereOn

Відповіді:


281

Перше, що ви не використовуєте таку структуру даних. Якщо вам потрібна тривимірна матриця, ви визначаєте одну:

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

Або якщо ви хочете індексувати за допомогою [][][], вам потрібен operator[] який повертає проксі.

Після того, як ви зробите це, якщо виявите, що вам потрібно постійно повторювати, як ви подали, ви відкриєте ітератор, який підтримуватиме його:

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

Тоді ви просто пишете:

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

(або просто:

for ( auto& elem: m ) {
}

якщо у вас C ++ 11.)

І якщо вам потрібні три індекси під час таких ітерацій, можливо створити ітератор, який викриває їх:

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);
        }
        //  ...
    };
};

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

5
це може бути правдива відповідь, але я не згоден, що це хороший. багато криптовалютних кодів шаблонів з, ймовірно, x10 разів повільним часом компіляції та, ймовірно, x10 повільним налагодженням (може бути більше) кодом. Для мене, безумовно, оригінальний код є для мене більш зрозумілим ...
Gorkem

10
@beehorf ... а також набагато, набагато повільніше. Оскільки багатовимірні масиви в C і C ++ насправді є вкладеними масивами в тому сенсі, що зовнішні розміри зберігають покажчики на вкладені масиви. Ці вкладені масиви потім довільно розкидаються по пам'яті, ефективно перемагаючи будь-яке попереднє завантаження та кешування. Я знаю приклади, коли хтось написав код, використовуючи vector<vector<vector<double> > >для представлення тривимірного поля. Перезапис коду, що відповідає рішенню, призвів до прискорення 10.
Майкл Уайлд

5
@beehorf Де ви бачите код шаблону? (На практиці, Matrix3Dмабуть , це має бути шаблон, але це дуже простий шаблон.) І вам доведеться налагоджувати лише Matrix3Dне кожен раз, коли вам потрібна 3D-матриця, тому ви економите величезну кількість часу на налагодженні. Щодо ясності: як std::vector<std::vector<std::vector<int>>>зрозуміліше Matrix3D? Не кажучи вже про те, що Matrix3Dнав'язує той факт, що у вас є матриця, тоді як вкладені вектори можуть бути нерівними, і що вищезгадане, ймовірно, значно швидше.
Джеймс Канзе

10
@MichaelWild Але, звичайно, справжня перевага мого підходу полягає в тому, що ви можете змінювати представлення залежно від того, що швидше у вашому середовищі, не змінюючи жодного коду клієнта. Запорукою хорошої продуктивності є належна капсуляція, щоб ви могли внести зміни, за якими профайлер вважає, що вам потрібно, не переписуючи всю програму.
Джеймс Канзе

44

Використання макросу для приховування forциклів може бути дуже заплутаним, просто для збереження кількох символів. Я б замість цього використав цикли діапазону :

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

Звичайно , ви можете замінити auto&з , const auto&якщо ви, справді, не змінюючи дані.


3
Якщо ОП може використовувати C ++ 11.
Jarod42

1
@herohuyongtao У випадку ітераторів. Що може бути тут ідіоматичнішим, але є випадки, коли вам потрібно три intзмінні.
Джеймс Канзе

1
І чи не так do_something_on_A(*j)?
Джеймс Канзе

1
@ Джеффрі Ага, так. Ще одна причина написання типу. (Я припускаю , що використання autoдля kі iможе бути виправдано , за винятком того , що до сих пір вирішує проблему на неправильному рівні, реальна проблема полягає в тому , що він з допомогою вкладених векторів ..)
Джеймс Kanze

2
@Dhara k- це весь вектор векторів (добре посилання на нього), а не індекс.
Якк - Адам Невраумон

21

Щось подібне може допомогти:

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

Для того, щоб зробити його N-ary, нам потрібна магія шаблону. Перш за все, нам слід створити структуру SFINAE, щоб розрізнити це значення чи контейнер. Реалізація за замовчуванням для значень та спеціалізацій для масивів та кожного з типів контейнерів. Як зазначає @Zeta, ми можемо визначити стандартні контейнери за вкладеним iteratorтипом (в ідеалі ми повинні перевірити, чи можна використовувати тип із діапазонною базою forчи ні).

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

Реалізація for_eachпрямолінійна. Функція за замовчуванням викликає 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);
 }

І спеціалізація називатиме себе рекурсивно:

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

І вуаля:

 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,
 }

Також це не буде працювати для покажчиків (масиви, виділені в купі).


@herohuyongtao з обмеженнями ми можемо реалізувати дві спеціалізації для Containerта для інших.
попік

1
@herohuyongtao Я зробив приклад передбачення K-arry.
зав’язав

1
@fasked: Використовуйте is_container : has_iterator<T>::valueз моєї відповіді, і вам не потрібно писати спеціалізацію для кожного типу, оскільки кожен контейнер повинен мати iteratortypedef. Сміливо використовуйте що-небудь із моєї відповіді, ваше вже краще.
Зета

@Zeta +1 для цього. Також, як я вже згадував Container, допоможе концепція.
попік

::iteratorне робить ітерабельного діапазону. int x[2][3][4]цілком ітерабельно, як struct foo { int x[3]; int* begin() { return x; } int* end() { return x+3; } }; я не впевнений, яку T[]спеціалізацію слід робити?
Якк - Адам Невраумон

17

Більшість відповідей просто демонструють, як C ++ можна скрутити в незрозумілі синтаксичні розширення, IMHO.

Визначаючи шаблони чи макроси, ви просто змушуєте інших програмістів розуміти біти заплутаного коду, розробленого для приховування інших бітів задушеного коду.
Ви змусите кожного хлопця, який читає ваш код, мати досвід роботи з шаблонами, лише щоб не робити свою роботу з визначення об'єктів з чіткою семантикою.

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

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

просто відповідає криптичному визначенню вектора вектора int без чіткої семантики.


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

ОНОВЛЕННЯ: Я знаю, що ви просили цього, але краще не використовувати це :)


5
Я знаю, що про це попросила ОП, але серйозно ... Це виглядає як дивовижний приклад затуманення. Припустимо, TRIPLE_FORбуло визначено в якомусь заголовку, що я маю думати, коли бачу `TRIPLE_FOR тут.
Джеймс Канзе

2
Так, я здогадуюсь, ти маєш рацію :) Я думаю, я залишу це тут лише як приклад того, що це можна зробити за допомогою макросів, але додам зауваження, що краще не робити цього :) Я просто прокинувся і вирішили використати це питання як невелику розминку для розуму.
FreeNickname

5

Одна ідея - написати ітерабельний клас псевдоконтейнерів, який "містить" набір усіх мультиіндексних кортежів, які ви будете індексувати. Тут немає втілення, тому що це займе занадто багато часу, але ідея полягає в тому, що ви повинні вміти писати ...

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.
}

найкраща відповідь тут imo.
davidhigh

4

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

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

Однак це (очевидно) дає нам неоднозначні помилки. Тому ми використовуємо SFINAE для визначення того, чи відповідає поточний вхід функції або ні

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

Тепер це обробляє контейнери правильно, але компілятор все ще вважає це неоднозначним для input_types, які можуть бути передані функції. Таким чином, ми використовуємо стандартний фокус C ++ 03, щоб він віддав перевагу першій функції над другою, також пропустив нуль, і зробимо ту, яку ми вважаємо за краще прийняти та int, а інша бере ...

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

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

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

Доказ складання та виконання тут і тут

Якщо ви хочете більш зручного синтаксису в C ++ 11, ви можете додати макрос. (Далі неперевірено)

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 << ", ";
     }
}

3

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

Якщо ви виконуєте одну і ту ж операцію над кожним елементом багатовимірного масиву, не піклуючись про положення кожного елемента, то ви можете скористатися тим, що масиви розміщуються у суміжних місцях пам'яті, і ставитесь до всього, що є одним великий одновимірний масив. Наприклад, якщо ми хотіли помножити кожен елемент на 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;
}

Зауважте, що використання вищезазначеного підходу також дозволяє використовувати деякі "належні" методи 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);

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



@ecatmur: Цікаво - я лише почав працювати, тому перевірте це та відповідно оновлюю / видалюю відповідь. Дякую.
icobod

@ecatmur: Я переглянув стандарт C ++ 11 (розділ 8.3.4), і те, що я написав, має працювати, і не виглядає незаконним (для мене). Надане вами посилання стосується доступу до членів поза визначеним розміром масиву. Хоча це правда, що я отримую адресу щойно минулого масиву, але це не доступ до даних - це для того, щоб забезпечити "кінець", так само, як ви можете використовувати вказівники як ітератори, "кінець" - один минулий останній елемент.
icobod

Ви фактично отримуєте доступ B[0][0][i]до i >= 3; це заборонено, оскільки він отримує доступ за межами (внутрішнього) масиву.
екатмур

1
Чіткіший спосіб IMO призначення кінця, якби Ви це мали зробити, це end = start + (xSize * ySize * zSize)
noggin182

2

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

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

Ну, такий підхід не є елегантним та гнучким, тому ми могли б упакувати весь процес у функцію шаблону:

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

Ця функція шаблону може бути виражена і у вигляді вкладених циклів:

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

І його можна використовувати, надаючи 3D-масив довільного розміру плюс ім'я функції, дозволяючи виведенню параметрів робити важку роботу підрахунку розміру кожного виміру:

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

Назустріч більш загальному

Але знову ж таки, йому не вистачає гнучкості, тому що він працює лише для 3D-масивів, але, використовуючи SFINAE, ми можемо виконувати роботу для масивів довільного виміру, спочатку нам потрібна функція шаблону, яка ітераціює масиви рангу 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);
    }
}

І ще один, який повторює масиви будь-якого рангу, виконуючи рекурсію:

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

Це дозволяє нам ітератувати всі елементи у всіх вимірах масиву довільних розмірів довільних розмірів.


Працювати з std::vector

Для декількох вкладених векторів рішення повторно збирає масив масиву довільного розміру довільного розміру, але без SFINAE: Спочатку нам знадобиться функція шаблону, яка повторює std::vectors і викликає потрібну функцію:

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

І ще одна шаблонна функція, яка ітералізує будь-який тип векторів і називає себе:

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

Незалежно від рівня вкладеності, iterate_allвикликте версію векторів, якщо версія векторних значень не відповідає кращому, таким чином закінчуючи рекурсивність.

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

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

Дивіться демо-версію тут .

Сподіваюся, це допомагає.


1

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

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)

1

Приклейте з вкладеними петлями!

Усі запропоновані тут методи мають недоліки з точки зору читабельності чи гнучкості.

Що станеться, якщо для обробки у зовнішньому циклі потрібно використовувати результати внутрішньої петлі? Що станеться, якщо вам потрібне значення із зовнішньої петлі у вашому внутрішньому циклі? Тут більшість методів "капсулювання" не вдається.

Повірте мені, я бачив кілька спроб «очистити» вкладені петлі, і врешті-решт виявляється, що вкладений цикл насправді є найчистішим і гнучким рішенням.


0

Однією з методик, яку я користувався, є шаблони. Наприклад:

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
}

Тоді ви просто зателефонуєте do_something_on_A(A)в свій основний код. Функція шаблону створюється один раз для кожного виміру, перший раз з T = std::vector<std::vector<int>>, вдруге з допомогоюT = std::vector<int> .

Ви можете зробити це більш загальним, використовуючи std::function(або функціональні об'єкти в C ++ 03) як другий аргумент, якщо хочете:

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

Тоді називайте це так:

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

Це працює, навіть якщо функції мають однаковий підпис, оскільки перша функція краще відповідає будь-якому std::vectorтипу.


0

Ви можете генерувати індекси в одному циклі, як цей (A, B, C - розміри):

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

Я згоден з вами, він спеціально розрахований на 3 виміри;)
janek

1
Не кажучи вже про це неймовірно повільно!
noggin182

@ noggin182: питання було не про швидкість, а про те, щоб уникнути вкладених циклів; крім того, там є непотрібні підрозділи, i / (B * C) можна замінити на
janek

Гаразд, це альтернативний спосіб, мабуть, більш ефективний (javascript): для (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); }
янек

0

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

Для вашого першого прикладу я би переписав це як:

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

Це своєрідно підштовхує це, оскільки ви викликаєте функції у зовнішніх циклах, що еквівалентно розміщенню в них операторів. Я видалив усі непотрібні пробіли, і це може бути прохідним.

Другий приклад набагато кращий:

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

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

Якщо ви дійсно хочете макрос, ви можете зробити цей крок далі з чимось на зразок:

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

що змінило б другий приклад на:

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

і перший приклад також дешевший:

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

Сподіваємось, ви зможете досить легко сказати, які твердження йдуть, а які - для тверджень. Крім того, остерігайтесь коми, тепер ви не можете використовувати їх в одному пункті жодного з fors.


1
Читання їх жахливе. Заклинювання більше однієї forпетлі на лінії не робить її більш читаною, вона робить її меншою .

0

Ось реалізація C ++ 11, яка обробляє все можливе. Інші рішення обмежуються контейнерами з ::iteratortypedefs або масивами: але for_eachце стосується ітерації, а не контейнера.

Я також виділяю SFINAE на одне місце в is_iterableрисі. Диспетчеризація (між елементами та ітерабелями) здійснюється за допомогою диспетчеризації тегів, що я вважаю більш чітким рішенням.

Контейнери та функції, застосовані до елементів, ідеально передаються вперед, дозволяючи як constі не constмати доступу до діапазонів і функторів.

#include <utility>
#include <iterator>

Функція шаблону, яку я реалізую. Все інше може перейти в простір імен деталей:

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

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

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

Це якась котельня, необхідна для написання is_iterable. Я роблю пошук залежно від аргументів у просторі імен beginта endв деталях. Це імітує те, що for( auto x : y )цикл працює досить добре:

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;

TypeSinkКорисно перевірити , якщо код дійсний. Ви робите TypeSink< decltype(код, ) >і якщо значення codeдійсне, вираз є void. Якщо код недійсний, SFINAE починається, а спеціалізація блокується:

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

Я лише тестую на begin. adl_endТест також може бути зроблено.

Остаточна реалізація for_each_flatзакінчується надзвичайно просто:

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

Живий приклад

Це внизу: не соромтеся брати участь у найвищих відповідях, які є суцільними. Я просто хотів використовувати кілька кращих прийомів!


-2

По-перше, не слід використовувати вектор векторів. Кожен вектор гарантовано має суміжну пам'ять, але "глобальної" пам'яті вектора векторів немає (і, мабуть, не буде). Ви також повинні використовувати стандартний масив бібліотеки замість масивів у стилі С.

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

Але ще краще, ви можете визначити простий клас матриці 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;
}

Ви можете піти далі і зробити це повністю const-правильним, додати матричне множення (правильне та елементарне), множення на вектори тощо. Ви навіть можете узагальнити його на різні типи (я зробив би його шаблоном, якщо ви в основному використовуєте парні) .

Ви також можете додати проксі-об'єкти, щоб ви могли зробити B [i] або B [i] [j]. Вони могли б повернути вектори (в математичному сенсі) і матриці, повні подвійного &, потенційно?

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