Сегодня необычный для меня формат статьи: я скорее задаю вопрос залу, нежели делюсь готовым рецептом. Впрочем, для инициирования дискуссии рецепт тоже предлагаю. Итак, сегодня мы поговорим о чувстве прекрасного.
Я довольно давно пишу код, и так вышло, что практически всегда на C++. Даже и не могу прикинуть, сколько раз я написал подобную конструкцию:
for (int i=0; i<size; i++) {
[...]
}
Хотя почему не могу, очень даже могу:
find . \( -name \*.h -o -name \*.cpp \) -exec grep -H "for (" {} \; | wc -l
43641
Наш текущий проект содержит 43 тысячи циклов. Проект пилю не я один, но команда маленькая и проект у меня не первый (и, надеюсь, не последний), так что в качестве грубой оценки пойдёт. А насколько такая запись цикла for
хороша? Ведь на самом деле, важно даже не то количество раз, когда я цикл написал, а то количество раз, когда я цикл прочитал (см. отладка и code review). А тут речь очевидно идёт уже о миллионах.
На КПДВ узел под названием «совершенная петля» (perfection loop).
Так каков он, совершенный цикл?
А в чём проблема?
Мы пишем много кода для математического моделирования; код довольно плотный, с огромным количеством целых чисел, которые являются индексами ячеек, функций и прочей лабуды. Чтобы был понятен масштаб проблемы, давайте я просто приведу крохотный кусочек кода из нашего проекта:
for (int iter=0; iter<nb_iter; iter++) { // some iterative computation
for (int c=0; c<mesh.cells.nb(); c++) // loop through all tetrahedra
for (int lv0=0; lv0<4; lv0++) // for every pair of
for (int lv1 = lv0+1; lv1<4; lv1++) // vertices in the tet
for (int d=0; d<3; d++) { // do stuff for each of 3 dimensions
nlRowScaling(weight);
nlBegin(NL_ROW);
nlCoefficient(mesh.cells.vertex(c, lv0)*3 + d, 1);
nlCoefficient(mesh.cells.vertex(c, lv1)*3 + d, -1);
nlEnd(NL_ROW);
}
[...]
}
У нас есть некая область, разбитая на тетраэдры, и мы на ней моделируем некий процесс. Для каждой итерации процесса мы проходимся по всем тетраэдрам сетки, затем по всем вершинам каждого тетраэдра, бла-бла-бла, и всё венчает цикл по всем трём измерениям нашего окружающего мира.
Мы обязаны иметь дело с кучей вложенных циклов; вышеприведённые пять вложенных далеко не предел. Мы уже довольно давно (лет пятнадцать как) пришли к выводу, что стандартный for (int i=0; i<size; i++)
— это очень громоздкая конструкция: те самые пять вложенных заголовков for
превращаются в совершенно нечитаемую кашу, и даже подсветка синтаксиса не спасает.
Когда мы читаем стандартный for(;;)
, мы должны на каждой строчке обратить внимание на три вещи: на инициализацию, на условие выхода и собственно на инкремент. Но ведь это совершеннейший оверкилл для тех случаев, когда нам нужно пройтись от 0
до size-1
, а это подавляющее большинство всех циклов. Скажите, как часто вам приходится писать обратный цикл или итерацию с другими границами? Как мне кажется, один раз из десяти — это ещё щедрая оценка.
До появления c++11 мы в итоге пришли к страшной вещи, а именно ввели в самый верхний заголовок вот такой дефайн:
#define FOR(I,UPPERBND) for(int I = 0; I<int(UPPERBND); ++I)
И тогда вышеприведённый кусок кода превращается из тыквы в кабачок:
FOR(iter, nb_iter) {
FOR(c, mesh.cells.nb())
FOR(lv0, 4)
for (int lv1 = lv0+1; lv1<4; lv1++)
FOR(d, 3) {
nlRowScaling(weight);
nlBegin(NL_ROW);
nlCoefficient(mesh.cells.vertex(c, lv0)*3 + d, 1);
nlCoefficient(mesh.cells.vertex(c, lv1)*3 + d, -1);
nlEnd(NL_ROW);
}
[...]
}
Польза такой трансформации в том, что когда я встречаю for (;;)
, я знаю, что мне нужно насторожиться и внимательно смотреть на все три места (инициализацию, условие, инкремент). В то время как если я вижу FOR(,)
то это совершенно стандартный пробег от 0
до n-1
без каких-либо тонкостей. Я совершенно не предлагаю пользоваться вышеприведённым дефайном, но точно знаю, что для нашей команды он сберёг много ресурсов мозга, поскольку мы кода гораздо больше читаем (см. отладка), нежели пишем (как, наверное, и все программисты).
То есть, вопрос, которым я задаюсь, звучит так: "Как выглядит цикл, имеющий минимальную когнитивную нагрузку при чтении кода?"
Жизнь после 11го года, или range for
А как дела обстоят у соседей? Вы знаете, местами довольно недурно. Например, в лагере питонистов стандартный цикл выглядит следующим образом:
for i in range(n):
print(i)
Что любопытно, до третьего питона range()
создавал в памяти массив индексов, и проходился по нему. И со времён c++11 мы вполне можем делать точно так же!
#include <iostream>
int main() {
int range[] = {0,1,2,3,4,5};
for (int i : range) {
std::cerr << i;
}
}
Разумеется, явно создавать в памяти массив индексов это несерьёзно, и с третьей версии в питоне это тоже поняли. Но и в C++ мы можем сделать не хуже!
Давайте посмотрим на следующую функцию range(int n)
:
#include <iostream>
constexpr auto range(int n) {
struct iterator {
int i;
void operator++() { ++i; }
bool operator!=(const iterator& rhs) const { return i != rhs.i; }
const int &operator*() const { return i; }
};
struct wrapper {
int n;
auto begin() { return iterator{0}; }
auto end() { return iterator{n}; }
};
return wrapper{n};
}
int main() {
for (int i: range(13)) {
std::cerr << i;
}
return 0;
}
Пожалуйста, не начинайте int
vs size_t
, разговор не об этом. Если скомпилировать этот код при помощи gcc 10.2 с флагами компиляции -std=c++17 -Wall -Wextra -pedantic -O1
, то мы получим следующий ассемблерный код (проверьте тут):
[...]
.L2:
mov esi, ebx
mov edi, OFFSET FLAT:_ZSt4cerr
call std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
add ebx, 1
cmp ebx, 13
jne .L2
[...]
То есть, компилятор начисто убрал все эти обёртки и оставил голый инкремент, ровно как если бы мы написали обычный for (int i=0; i<13; i++)
.
Лично мне кажется, что for (int i: range(n))
справляется с подчёркиванием обычности цикла чуть хуже, нежели FOR(,)
, но тоже вполне достойно, и за это не нужно платить дополнительными тактами процессора.
Продолжаем подглядывать в замочную скважину: enumerate
Range for в c++11 нанёс большую пользу. Давайте скажем, что у меня есть массив трёхмерных точек, и мне нужно распечатать икс координаты каждой точки, это можно сделать следующим образом:
#include <vector>
#include <iostream>
struct vec3 { double x,y,z; };
int main() {
std::vector<vec3> points = {{6,5,8},{1,2,3},{7,3,7}};
for (vec3 &p: points) {
std::cerr << p.x;
}
return 0;
}
for (vec3 &p: points)
это прекрасная конструкция, никаких костылей, сразу из стандарта языка. Но что если у меня каждая точка из массива имеет цвет, вес или вкус? Это можно представить ещё одним массивом того же размера, что и массив точек. И тогда для доступа к атрибуту мне всё же понадобится индекс, который мы можем сгенерировать, например, вот таким образом:
std::vector<vec3> points = {{6,5,8},{1,2,3},{7,3,7}};
std::vector<double> weights = {4,6,9};
int i = 0;
for (vec3 &p: points) {
std::cerr << p.x << weights[i++];
}
Для этого кода компилятор генерирует следующий ассемблер:
.L2:
movsd xmm0, QWORD PTR [r13+0]
mov edi, OFFSET FLAT:_ZSt4cerr
call std::basic_ostream<char, std::char_traits<char> >& std::basic_ostream<char, std::char_traits<char> >::_M_insert<double>(double)
movsd xmm0, QWORD PTR [rbp+0]
mov rdi, rax
call std::basic_ostream<char, std::char_traits<char> >& std::basic_ostream<char, std::char_traits<char> >::_M_insert<double>(double)
add rbp, 8
add r13, 24
cmp r14, rbp
jne .L2
В принципе, имеет право на жизнь, но гулять так гулять, давайте снимем с программиста заботу о создании параллельного индекса, ровно как сделали в питоне, благо стандарт c++17 имеет structural binding!
Итак, можно сделать следующим образом:
#include <vector>
#include <iostream>
#include "range.h"
struct vec3 {
double x,y,z;
};
int main() {
std::vector<vec3> points = {{6,5,8},{1,2,3},{7,3,7}};
std::vector<double> weights = {4,6,9};
for (auto [i, p]: enumerate(points)) {
std::cerr << p.x << weights[i];
}
return 0;
}
Функция enumerate()
определена в следующем заголовочном файле:
#ifndef __RANGE_H__
#define __RANGE_H__
#include <tuple>
#include <utility>
#include <iterator>
constexpr auto range(int n) {
struct iterator {
int i;
void operator++() { ++i; }
bool operator!=(const iterator& rhs) const { return i != rhs.i; }
const int &operator*() const { return i; }
};
struct wrapper {
int n;
auto begin() { return iterator{0}; }
auto end() { return iterator{n}; }
};
return wrapper{n};
}
template <typename T> constexpr auto enumerate(T && iterable) {
struct iterator {
int i;
typedef decltype(std::begin(std::declval<T>())) iterator_type;
iterator_type iter;
bool operator!=(const iterator& rhs) const { return iter != rhs.iter; }
void operator++() { ++i; ++iter; }
auto operator*() const { return std::tie(i, *iter); }
};
struct wrapper {
T iterable;
auto begin() { return iterator{0, std::begin(iterable)}; }
auto end() { return iterator{0, std::end (iterable)}; }
};
return wrapper{std::forward<T>(iterable)};
}
#endif // __RANGE_H__
При компиляции с флагами -std=c++17 -Wall -Wextra -pedantic -O2
мы получим следующий ассемблерный код (проверьте тут):
.L14:
movsd xmm0, QWORD PTR [rbx]
mov edi, OFFSET FLAT:_ZSt4cerr
call std::basic_ostream<char, std::char_traits<char> >& std::basic_ostream<char, std::char_traits<char> >::_M_insert<double>(double)
mov rdi, rax
mov rax, QWORD PTR [rsp+32]
movsd xmm0, QWORD PTR [rax+rbp]
call std::basic_ostream<char, std::char_traits<char> >& std::basic_ostream<char, std::char_traits<char> >::_M_insert<double>(double)
add rbx, 24
add rbp, 8
cmp r12, rbx
jne .L14
И снова компилятор начисто убрал обёртку (правда, для этого пришлось поднять уровень оптимизации с -O1
на -O2
).
Кстати, в c++20 появился std::ranges
, что ещё больше упрощает написание такой функции, но я пока не готов переходить на этот стандарт.
Вопрос залу
На ваш взгляд, каким должен быть совершенный цикл в 2020м году? Научите меня!
Если вы ещё не задавались этим вопросом, то скопируйте к себе в пет-проект заголовочный файл range.h
и попробуйте его поиспользовать хотя бы несколько дней.
amarao
Во втором питоне был xrange (итератор). В целом,
for in
— это один из элементов хорошей эргономики языков, который появился относительно поздно.В Rust его эргономику подняли ещё выше. Не теряя общности протокола итерации, они сделали так:
Более того, зацените с inclusive:
(досчитает до 10 включительно)
А ещё много всего вкусного: https://doc.rust-lang.org/reference/expressions/range-expr.html
haqreu Автор
Поэтому я и смотрю в сторону range for, но мне важно понять, как писать понятные циклы именно на цпп, т.к. вопрос перехода на что-то другое у нас не встанет ещё долго.
dreesh
Пример с выводом X в Haskell.
ЗЫ: Только я не настоящий сварщик.
ЗЫ: запускал в интерпретаторе ghci
akryukov
Это мы еще не вспоминали (0..10).map(function)
AndreiVorobev
Как мне кажется, в используемом автором примере интереснее всего использовать zip()
haqreu Автор
А вот за zip спасибо! Забыл про него. Надо подумать, как бы поэлегантнее его сделать с переменным количеством аргументов (впрочем, сильно вряд ли кто-то захочет зипить больше трёх массивов, уж больно громоздкая запись будет).
FoxCanFly
www.boost.org/doc/libs/1_72_0/libs/iterator/doc/zip_iterator.html
haqreu Автор
Да, я уже посмотрел. В целом стараюсь стоять от буста подальше, хотя некоторые вещи оттуда можно перенять.
chersanya
Эргномика выше по сравнению с чем? Во многих языках такой синтаксис есть, с разными мелкими отличиями типа
:
вместо..
. Да даже в питоне, который намного старше раста, выглядит похоже:for x in range(10)
, илиfor x in range(0, 10)
если хотите нижнюю границу указывать.mayorovp
По сравнению с
for (int i = 0; i < ...; i++)
chersanya
Так в первом комментарии было написано мол "вот есть обычный for-in, а в расте ещё эргономичнее его сделали". Вы же сравниваете не с for-in циклом.
Ну а сам
for-in-range
в питоне уже лет 20 существует, если не больше — думаю, даже это странно считать "относительно поздно" в плане развития языков программирования. Уверен, что в каких-то других языках такое и раньше было сделано.amarao
while/for в языках появился примерно на 20 лет раньше питона, насколько я понимаю. То есть я говорю, что конструкция for in появилась существенно позже, чем обычный for, то есть это относительно новое улучшение эргономики.
amarao
Эргономика выше за счёт добавления range в синтаксис языка. 2..4 явно выразительнее, чем range(2,4). Букв меньше, скобок меньше.
lain8dono
В Rust лучше использовать итераторы. Одновременно и читабельнее и быстрее. Не лишним будет упомянуть rayon (docs.rs). Многопоточные числодропилки в пару строчек можно делать:
peacecoder85
Очень "интересная" логика. По умолчанию, если выражаться математикой цикл [0,10). В расте об эргономике вообще не думают? Обычно сахар в виде ".." подразумевает полное включение от и до
amarao
Думают, думают.
В большинстве случаев надо считать от нуля до N-1. Даже у автора поста именно этот случай описан.
Причина — если у вас есть N элементов, то у них номера от нуля до N-1.
Понятно, что математику непривычно. Но математику так же не привычно, что в компьютере нет ни натуральных, ни вещественных чисел. А ещё нельзя досчитать до бесконечности (в обоих направлениях).
AnthonyMikh
Во-первых, откуда "обычно", во-вторых, на практике полувключающие диапазоны удобнее
chersanya
Посмотрел таки ради интереса по вашей ссылке — эта штука с двумя точками в расте очень ограниченная, ничего не поменялось с тех пор как я с ним игрался:
AnthonyMikh
С учётом того, что
step_by
работает не только на численных диапазонах, а на произвольных итераторах, не вижу, в чём проблема.После стабилизации Step можно будет реализовать его для своих типов и использовать для нативных диапазонов. Если nightly не пугает, то можно и сейчас.
chersanya
Ну так соответствующая операция на итераторах в любом языке наверное есть, в котором итераторы используются. Даже в питоне — islice ровно это делает.
В общем, никак не понимаю чем итераторы именно в расте удобнее/эргономичнее, как тут выше писали — по-моему они совершенно обычные, возможности такие же как везде.
О, это хорошо, рад за раст :)
Я растом по факту не пользуюсь, только игрался и посматриваю иногда на концептуальном уровне. Лично для меня он встал в категорию языков "придуманы и реализованы интересные/полезные идеи, но мне не подходит" — там же и например lisp, haskell. Мне для всяких вычислений, статистики и анализа данных (+ остальное по мелочи) главное — экосистема, которая в расте по сути отсутствует; ещё слишком многословно выглядит всё-таки, и слишком "низкоуровневый".
fareloz
Когда-то давно тоже об этом думал в том же ключе — макросы, коллекции индексов и так далее:
habr.com/ru/post/264803
ultrinfaern
Почему вы не пошли с for дальше — в большинстве случаев вам индекс не нужен, вам нужно перечисление элементов. То есть следующий этап foreach.
haqreu Автор
Вы знаете, в большинстве случаев у меня даже основного массива элементов нет. Например, для конкретного дома номер 5 по Садовой улице у меня есть массив имён ответственных квартиросъёмщиков, есть булевский массив (того же размера) наличия в квартире счётчика холодной воды, целочисленный массив (опять же того же размера) задолженности по оплате лифта и тому подобное, то есть, куча атрибутов для каждой квартиры. А вот непосредственно массива квартир у меня нет, они неявно присутствуют в индексе массивов. Поэтому `std::for_each` для меня тупиковый путь. Мне нужна возможность получить доступ ко всем данным для каждой взятой квартиры.
ratijas
Звучит как
zip()
+filter()
или дажеfilter_map()
. С хорошей базой итераторов можно много делов наворотить.nin-jin
Вам скорее нужна правильная абстракция, позволяющая работать со структурой массивов, как с массивом структур.
haqreu Автор
Да, возможно. Есть идеи помимо зипа?
nin-jin
Специальный контейнер, возвращающий прокси, которые берут данные из массивов по заданному смещению.
haqreu Автор
Учитывая, что у меня массивов каждый раз разное количество и каждый разного типа, я пока вижу только зип, который возвращает std::tuple для данного смещения…
nin-jin
Ну да, в этом случае только zip.
chersanya
Лучше не tuple, а namedtuple (есть такое в C++?), или struct — чтобы обращаться нормально по имени, типа a[i, j].cost. Не скажу за C++, но я иногда пользуюсь таким виртуальным представлением struct-of-arrays в виде array-of-structs — удобно, когда работающая с ним функция особо не заморачивается, какой из этих двух вариантов ей передали.
haqreu Автор
tuple можно легко разобрать на запчасти при помощи structural binding, так что имена будут красивыми.
AnthonyMikh
Только если типы в кортеже одинаковые, то их легко перепутать
haqreu Автор
Ну, если запись вида
то перепутать сложно. А вот если объявление кортежа дальше, то да, можно перепутать. Но тут мы говорим именно о таком локальном объявлении.
chersanya
Я имел в виду какой-то такой сценарий, в котором обычно пользуюсь SoA-as-AoS. Есть функция, которая обрабатывает массив структур:
Ещё есть функция, которая работает с обычными числовыми массивами:
И нужно несколько раз по очереди их применять к данным одного и того же массива. Тогда пишу примерно так:
Тут важно, чтобы элементы A_s представлялись как структуры с правильными названиями полей, иначе f(A_s) не сработает.
Вообще очень удобно получается такая абстракция — код чище, функция может принимать один массив вместо условных пяти, которые должны быть одного размера.
gotoxy
del
dimoff66
Лучше всего со вложенными циклами дела обстоят у scala, в одной конструкции for делаются все уровни:
output:
0, 1, a
0, 1, b
0, 1, c
0, 2, a
0, 2, b
0, 2, c
…
amarao
Я скалу не знаю, но глядя на этот синтаксис у меня есть сразу же куча интерпретаций:
for x in zip()
) до момента пока все итерируются.for x in zip()
) до момента пока хотя бы одна итерируется.Так что не очень выразительно.
dimoff66
Главное ни в коем случае не смотрите на output под кодом, а то неинтересно когда вместо четырех интерпретаций остается только одна.
haqreu Автор
Вы знаете, но если нужно смотреть на вывод для того, чтобы понять, что делает кусок кода, это тревожный звоночек. Впрочем, scala и rust это интересно, но меня интересует как улучшить читаемость кода именно на C++ в рамках уже существующих стандартов. Хотя, конечно, примеры из параллельных вселенных интересно изучить, вдруг что можно перенять…
dimoff66
Вы просто написали что подглядываете за соседями, я вас познакомил с еще одним соседом. Синтаксис может казаться непривычным, с первого взгляда код скалы и вправду понять непросто, но есть такое расхожее мнение — кто начинает писать на скале, на других языках писать больше не хотят. Так что это просто пример, согласитесь что
выглядит куда компактней, чемПо крайней мере это выглядит любопытно
haqreu Автор
Может оно и выглядит любопытно, но мне кажется всё же нечитаемым. По крайней мере, мне нужно конкретно щуриться и выделять текст блоками, чтобы понять, что происходит в этом цикле. Кроме того, не забывайте, что в реальном коде не будет простых границ как в вашем примере, это будут другие переменные/константы/вызовы функций, что читаемости не прибавит никак.
В сях тоже можно при желании вложенные циклы сделать в рамках одной конструкции for(;;). Но это как с тем троллейбусом: зачем?
andr1983
На самом деле там 3 конструкции, for не более чем сахар и написать можно еще так:
haqreu Автор
О, так читается гораздо лучше. Но я хочу идти ещё дальше, я хочу, чтобы самый частый цикл не имел даже упоминания о нижней границе. Только имя переменной и верхняя граница. Именно поэтому в моей имплементации `range(int n)` имеет только один аргумент, а не три, как в питоне. Если вдруг мне понадобится пробежать не 0..n-1, у меня будет цикл другого вида, без слова `range`, и это привлечёт моё внимание.
FTOH
Может тогда к такому нужно стремится
FTOH
Сделал на JS, получилось в 30 раз медленнее вложенных циклов
mayorovp
Ну, это же JS, тут не бывает zero-cost абстракций.
subcommande
А где такое можно провернуть с zero-cost? Не представляю, как здесь constexpr использовать.
mayorovp
А причём тут constexpr?
subcommande
Как самый очевидный (для меня) пример zero-cost приема, ведь выражение с constexpr не вычисляется в рантайме.
nin-jin
https://dlang.org/phobos/std_algorithm_setops.html#.cartesianProduct
Akon32
Можно создать implicit класс для чисел или любого другого типа, и потом писать что-то вроде
for (i<-5.times){...}
или5 times { .... }
.Но более привычно выглядят варианты
for(i<-0 until 5){...}
иfor(i<- array.indices){ ... }
.Jesting
а как на скале написать с++ аналог:
for(int = i=0,j=0; i<5;i++,j++) — цикл один, а переменных несколько?
как не спутать с тем что в примере с вложенными.
chersanya
По-хорошему такой цикл через zip делается, хоть на скале хоть на чём:
for (i, j) in zip(0:4, 0:4)
с поправками на детали синтаксиса разных языков.andr1983
Как-то так (можно и в одну строчку свернуть):
Но, с большой вероятностью, за такой код в scala сообществе закидают тапками )
vadrer
оно вам видится нечитаемым потому что вы не знакомы с этой парадигмой. если привыкнете ней — этот код будет намного более читаемым, и щуриться не надо.К тому же короче, что тоже помогает чтению при прочих равных.
v1000
С одной стороны, визуально кажется что там отрицательное значение -1
С другой стороны, возможен баг за счет копипасты
Akon32
Для более полной демонстрации циклов scala я бы привёл пример посложнее:
И это ещё без yield (который в scala довольно убогий по использованию и производительности).
Wyrd
/ sarcasm
В 2020 должна быть функциональщина с хвостовой рекурсией вместо цикла /
Ну а если серьезно — зависит от требований и контекста. Если не нужна оптимальная производительность, то можно действительно использовать функциональщину или что-то типа LINQ из С# аля method chaining (не знаю, завезли ли такой синтаксис в плюсы уже наконец-то). Если производительность таки важна, можно попробовать вынести циклы выше 3-го уровня вложенности в отдельную функцию, в надежде что умный компилятор все заинлайнит. Желательно вынести так чтоб это ещё и имело какой-то «физический смысл», тобишь было очевиден новичку в проекте.
Ну и да, я лично целиком поддерживаю ваше желание убрать «лишние буквы» — чем меньше букв тем проще читать код (обычно, бывают исключения).
haqreu Автор
Мне крайне важна производительность, мы пишем конкурентноспособные числодробилки, так что любая абстракция пойдёт до тех пор, пока компилятор её способен воплотить при нулевой стоимости по сравнению с обычным си. Разумеется, мы выносим внутренности в отдельные функции, но это не всегда возможно.
hddscan
прошу ногами сильно не бить, но «совершенная петля» это «perfect loop», а «perfection loop» это «петля совершенства».
haqreu Автор
А чего ногами-то бить. Я привёл названия (причём со ссылкой) на двух языках, переводил не я. И я не уверен, что в русский язык название пришло из английского, а не, скажем, голландского…
Tangeman
"perfection" в данном случае неоднозначно — можно перевести как "петля совершенствования", c учётом контекста статьи очень даже интересная игра слов получается.
adeshere
Мне одному кажется, что если значения можно разместить в массивах, то самое удобное оформление вложенных циклов реализовано в фортране? Например:
real a(10,10,10), b(10,10,10), c(5)
integer i,j,k
forall (i=1:10, j=2:10:2, k=6:8, b(i,j,k) /= 0)
a(i,j,k) = 3 * i / b(i,j,k) + c(j/2)
end forall
haqreu Автор
Я не уверен, что это самое удобное. Кроме того, мне нужно понять, как жить дальше именно c C++ :)
poulch
по старинке жить надо. какой смысл в новомодных извращениях если они не дают выигрыша в скорости судя по асму. попытки сокращения, внесения всяких символов и повышения смысловой нагрузки в конструкциях языка несет только одно — ухудшение читаемости и понимаемости кода обычным человеком. тупо повышается порог вхождения в профессию и стоимость услуг. изначально смысл появления языков высокого уровня был в снижении этого порога.
haqreu Автор
Выиграть в скорости у обычного for(;;) вряд ли удастся. А с утверждением, что любая попытка ввести стиль программирования приводит к ухудшению читаемости, я не согласен категорически. Вы примеры из статьи смотрели?
poulch
естественно я всю статью прочитал. я про стили ничего не говорил. у меня стиль вообще своеобразный тк я все время был одиночкой и форматирую свой текст для своего удобства. {}=>[]<=() когда этого становиться много в языке это зло. но я начинал с паскаля на см-4… и предпочитаю легко читать и писать код, а не разгадывать символьные ребусы и запоминать вычурные конструкции языка.
haqreu Автор
Вы о каких вычурных конструкциях-то? У меня нет предложения реорганизовать рабкрин (простите, стандарт c++). У меня есть конкретный вопрос о том, как писать на современном c++ таким образом, чтобы это экономило усилия разработчика в рамках одной команды. То есть, о стиле программирования в рамках одного проекта. У нас текучка небольшая, и мне несложно объяснить стиль каждому новому человеку.
poulch
мне непонятно зачем читать эти циклы. почему они не в функциях с нормальным говорящим названием.
haqreu Автор
Если мне нужно пробежать все ячейки кубической сетки, то у меня по-любому будет три вложенных цикла, будь они разнесены по функциям или нет.
poulch
ну и зачем их читать. оформили в функцию с параметрами и читайте эти параметры в одной строчке. по мне так это вопрос уровня создания меню. пишется и вызывается MakeMenu(). А уже MakeMenu совершенно тупая функция в которой куча InsertItem(). ну или я чего-то не понимаю.
haqreu Автор
Оформление для грядущих поколений это хорошо. Но для начала код нужно написать и отладить. И я вас уверяю, во время отладки этот код читается много-много-много раз.
chersanya
Не обязательно три вложенных цикла — можно по массиву этих ячеек в линейном порядке проходить. Если проходимая структура более сложная, чем многомерный массив, то казалось бы можно один раз реализовать функцию обхода по ней с индексом, а потом эту функцию только использовать как аргумент в for-in. Или у вас такое решение по какой-то причине не проходит?
haqreu Автор
Давайте забудем про три вложенных, скажем, мне нужно два вложенных для обработки изображения. Да, я могу пройтись одним линейным циклом по всем пикселям изображения, но если мне нужно каждый пиксель усреднить с его четырьмя соседями, то мне нужны индексы (i,j) для каждого пикселя. Да, я смогу их получить целочисленным делением и взятием по модулю из линейного индекса, но насколько это оправдано?
chersanya
По-моему такой сценарий абсолютно типичный и не требует написания вложенных циклов по размерностям каждый раз. Один раз реализуете функцию обхода-по-индексам если её нет, и потом эту функцию используете. Я бы примерно так сделал (пример на джулии, т.к. в современном c++ почти не ориентируюсь; но код не завязан фундаментально на язык):
При этом, как видно из кода, очень легко обобщить на произвольное понимание "соседей" клетки, на многомерные массивы, и т.п. — явно удобнее, чем вложенный цикл на каждую размерность массива, и захардкоженные координаты соседей.
nin-jin
Зачем вам этот дедушка с его неизлечимыми проблемами?
haqreu Автор
Простите, а какие варианты? Раст?
Rogaven
Дмитрий $mol наверное предложит
mayorovp
$mol он для веба предлагает, а тут он предложит D (это язык такой, не смайлик)
haqreu Автор
И ведь действительно предложил :)
AnthonyMikh
Хороший вариант, между прочим.
chersanya
Запись вложенных циклов одной строкой — согласен, удобная фича, тоже часто пользуюсь. Но почти больно :) смотреть на повторение i,j,k трижды в вашем блоке кода: иногда попадался на опечатках в таких случаях, особенно если индексы массива из переменных цикла получаются сложнее (типа
a[i + 2, j, k * 3]
), или идут в другом порядке, или надо будет потом подправить это место. Поэтому теперь стараюсь писать так (пример на julia), т.е. не повторяя много раз получение индекса массива:BD9
Но ведь от этого отказываются?
Deleted and Obsolescent Language Features
Без forall и where, просто do concurrent.
stackoverflow.com/questions/4122099/do-fortran-95-constructs-such-as-where-forall-and-spread-generally-result-in-fa/4141572#4141572
stackoverflow.com/questions/8602596/fortran-forall-restrictions
Сложные расчёты всё равно не для gcc, а ICC/IFC, Nvidia/PGI, IBM, Cray и прочие.
Там уже есть свои образцы использования for/forall/foreach…
parallel-for.sourceforge.net/index.html
parallel-for.sourceforge.net/parallelfor.html
Пример: берёте железо Nvidia, вам дают инструменты для его правильного использования.
Можно при желании доопределить свои конструкции — в рамках выбранной связки «железо + ПО». Но потом придётся разбираться что там компилятор наделал — насколько хорошо распараллелил.
Какой смысл делать свой местечковый стандарт? NIH?
haqreu Автор
Это вы сильно загнули. Опыт HPC с gcc имеете?
Мы не делаем местечкового стандарта, а думаем о стиле оформления проекта, а это в любой серьёзной конторе есть.
VarLegovar
Тоже не увидел смысла в этом
adeshere
В статье был намек на соседей… это просто пример. Причем из языка, который достаточно широко используется для моделирования, и по эффективности мало кому уступает. Что позволяет писать вычислительную часть на фортране и цеплять эти функции к проектам на других языках. Но у С++ эффективность сравнима с фортраном (если сравнивать хорошие компиляторы), поэтому переписывать уже имеющийся проект смысла нет, разумеется. Особенно с учетом отсутствия разработчиков, владеющих этим хорошим, но очень уж узконишевым языком…
А вопрос удобства всегда имеет субъективный оттенок, естественно. Но массивы и их сечения в современном фортране действительно очень мощные. А элементы ООП, которые в него тоже недавно (лет 20 назад) добавили, позволяют делать большие проекты с хорошо читаемым кодом. Если, конечно, там нет унаследованных из древности простыней на фортране-66 с вычисляемыми GOTO и прочими подобными ужасами ;-). Которые хотя и признаны устаревшими (и категорически не рекомендуются к использованию), но до сих пор поддерживаются компиляторами «для совместимости»…
haqreu Автор
Фортран хорошая штука, и даже для наших целей (матмоделирование), казалось бы, должен подходить. Но при ближайшем рассмотрении оказывается, что (современный) C++ гораздо удобнее (нам) даже несмотря на то, что мы отказались от сложных динамических структур данных и всё упаковываем в большие массивы.
adeshere
Если можно, было бы интересно узнать — по каким критериям удобнее?
haqreu Автор
Вы знаете, если отвечать предельно честно, то самый главный критерий это то, что практически никто не знает современного фортрана (например, стандарт 2018го года). Ну а вокруг этого можно навернуть всяких рассуждений о гибкости C++ и тому подобного. Лично мне удобно, что абсолютно один и тот же код может исполняться как на CPU, так и на GPU при помощи препроцессора (си/куда).
Как я и говорил, мы отказались от сложных структур данных, и все реальные данные упакованы в std::vector<>. Но поверх этих векторов у нас есть zero cost abstractions, которые облегчают разработку. Насколько я знаю (внимание, я не специалист), всякие shared pointer возможны в фортране, но это нужно конкретно над ними работать. В цпп они доступны из коробки. Так что гибкость я бы не сбрасывал со счетов.
Мне очень интересно, что вы думаете об областях примения фортрана и цпп.
adeshere
К сожалению, я здесь могу только спрашивать, а не отвечать, так как являюсь типичным динозавром, давно закостеневшим в развитии, и не вымершим вовремя только в силу какого-то недоразумения. Поэтому мой взгляд на этот вопрос совершенно односторонний…
Если подробно, то мы уже 30 лет пилим очень узконишевую программу для научных исследований, причем основная часть сделана на фортране, а интерфейс — на плюсах. Я сам программирую только на фортране, за плюсы отвечает другой человек (и его код я понимаю ровно настолько, чтобы вносить туда совершенно косметические изменения вроде исправления опечаток в комментах ;-)).
Но что касается именно перемалывания чисел, то у фортрана это получается очень даже неплохо. Второй важный нюанс — весь фортрановский код, написанный нами лет 20 назад, до сих пор не только работает, но и при необходимости модифицируется с расширением функционала, и это не требует сверхусилий. Хотя наверно это зависит не только от языка… но для данного конкретного применения фортран вполне адекватен.
haqreu Автор
Наверное, я не умею готовить фортран, но лично у меня гибкая и расширяемая база кода получается именно на цпп. Перегрузка операторов, шаблоны и прочая фигня мне сильно помогает. Компиляторы C++ наконец-то догнали фортрановские, и теперь аргумент скорости уже не работает. Наверное, всё же очень сильно зависит от того, что нужно писать. Я наш цпп код спокойно запускаю в браузере (emscripten), и мне это часто нужно. У фортрана с этим напряжёнка. Так что, чистые конечные элементы на фортране хорошо, но вокруг этого обвязки на цпп вряд ли удастся избежать. В итоге мы и FEM делаем на чистом C++.
Но повторюсь ещё раз: очень мало людей знакомы с современным фортраном, можно даже сказать что это молодой язык, как бы парадоксально это ни звучало.
vitaliy31
Antervis
haqreu Автор
Нет, у нас все массивы одномерные. Ну, всякие разреженные матрицы не считаем за двумерный массив :)
fareloz
Можно использовать прокси-объекты, которые будут ссылаться на строки в одномерном массиве. Правда есть накладные расходы на созданеи такого объекта
AAngstrom
Отвечу как человек, которому приходится много кодить на Фортране. (При этом не могу сказать, что являюсь большим фанатом этого языка. Почему — дальше по тексту). У Фортрана есть много преимуществ для численных расчётов, плюс язык вышел из комы, в которой он находился в 90-х, и на данный момент существует более-менее постоянная комиссия, которая занимается развитием стандарта. Но у этого стандарта есть, как минимум, две проблемы: 1). обратная совместимость требует тащить с современный язык всякие архаизмы; 2). другая проблема, характерная и для многих других соверменных языков, — нет какой-то чёткой цели развития. В итоге, многие новшества добавляются ad hoc, приводят к усложнению, которое не всегда окупается потенциальными преимуществами. К примеру, начавшееся со стандарта 2003 движение в сторону ООП сильно притормозилось, а использование этих черт языка в нынешнем их виде зачастую приводит к сильному проседанию производительности. И никаких улучшений в виде zero-cost abstractions в следующем стандарте 202х не планируется.
Поэтому ситуация на данный момент такова, что если у вас уже есть значительная кодовая база на Фортране (не обязательно legacy, может быть стандарт 90 или 2003), то переходить на C++ или что-то другое смысла нет (тем более, что у C++ своих подобных проблем со стандартом вагон и маленькая тележка), т.к. на современном Фортране можно относительно комфортно писать высокопроизводительный код, если смриться с некоторыми корявостями. Если же у вас проект уже написан на C++, то я бы точно не советовал переходить на Фортран. На мой взгляд, будущее обоих этих классических языков довольно туманно, поэтому это будет смена шила на мыло. Если же начинать проект с нуля, то стоит уже смотреть в сторону чего-нибудь типа Julia. К сожалению, этот язык ещё не выбрался из стадии альфа.
(Речь идёт о численных расчётах, поэтому rust, go и далее по списку — не предлагать.)
haqreu Автор
Это точно. Я пишу на C/C++ уже четверть века, и чем дальше, тем больше он меня пугает. Но внятной замены пока не вижу.
nin-jin
Чем D не внятен?
haqreu Автор
В нём сложно управлять памятью, например. Нет хороших компиляторов, отладчиков, профайлеров, IDE…
nin-jin
А в чём сложности?
А чем существующие плохи?
Можете использовать ту же Visual Studio.
haqreu Автор
C++ компиляторы только-только догнали фортран по производительности кода. D ещё далёк от этого.
А вообще D — это мертворожденный язык (по моему скромному мнению). Он был создан для того, чтобы «починить» ошибки дизайна C++. Проблема в том, что никто не может сказать внятно, в чём именно ошибки. И тем паче как их чинить.
Сборщик мусора — страшная штука. D захотел «починить» проблемы с указателями в C++. Но только сборщик мусора — не решение проблем с указателями...
nin-jin
С чего вы взяли? Там довольно крутой компилятор, позволяющий использовать разные бэкенды, в том числе и популярные у плюсовиков GCC да LLVM.
В C++ много кривых мест, начиная с использования препроцессора и заканчивая UB во всех местах. В любой книжке по D об этом всём рассказывается.
Конечно, поэтому вас его использовать никто не заставляет.
BeardedBeaver
Мне очень нравится D и очень жаль, что нет крупной корпорации, которая вкидывает газиллионы долларов в его популяризацию (надеюсь, это только пока). Но все вот эти вот @nogc и @betterC у меня вызывают легкую панику.
nin-jin
Почему? Аттрибуты функций — это вообще мощная штука, позволяющая выводить различные характеристики исходя из содержимого функций. И, наоборот, ограничивать поддерево вызовов нужной характеристикой. Того же
shared
иpure
очень не хватает в других языках.mayorovp
А что не так в rust с численными расчётами?
AAngstrom
Последний раз, когда я смотрел, там, например, не было доступно "из коробки" ни комплексных чисел, ни многомерных динамических массивов с сечениями и всеми сопутствующими вещами. Новый язык без этих фич — сразу в топку. Rust не для численных расчётов создавался, в конце концов, и это влияет на вектор развития.
haqreu Автор
Я бы сказал, что отсутствие комплексных чисел вообще не аргумент. В C++ их тоже нет, однако считают на нём только в путь. И многомерных массивов нет. И сечений...
AAngstrom
Я поэтому и написал про "новый язык". Понятно, что на C++ уже все привыкли к велосипедам. А комплексные числа там, в принципе, есть как часть стандартной бибилотеки. А с учётом стандарта C99 они ещё и частью языка являются (_Complex).
AnthonyMikh
Да всё там есть. Да, это сторонние библиотеки, но зачем это в сам язык-то пихать?
AAngstrom
Мы же про удобство для конкретных целей говорим. Так-то на любом языке можно решать любые задачи. Хоть на vanilla C. Только библиотеки подавай.
Вообще говоря, никак не хотел обидеть растаманов, но язык же явно с другим фокусом создавался. Системное программирование, контроллеры, веб и т.д. Не вижу, какие такие преимущества он может принести для численного моделирования по сравнению с Fortran/C++, чтобы всё бросать и разбираться во всех этих тонкостях области действия, (не)изменяемости, unsafe и прочих специфических приблуд. При этом, для элементарных, с точки зрения численных расчётов, вещей уже нужно тащить даже не стандартные, а какие-то непонятно кем написанные библиотеки, API которых может внезапано поменяться или поддержка которых вообще может прекратиться.
haqreu Автор
На всякий случай поясню комментарий про API: время жизни очень большой части софта для численных расчётов должно измеряться десятилетиями. Поэтому на новомодный сырой язык мало кто перейдёт только потому, что это молодёжно. Очень, очень не зря современный фортран тащит на себе ярмо совместимости с предыдущими версиями стандарта.
Нас, например, очень напрягает то, что современный C++ убирает кучу старых фич, т.к. это ломает код.
AnthonyMikh
Не совсем. Некоторые вещи, отсутствующие в языке, невозможно восполнить библиотеками. В C, например, напрочь отсутствует обобщённые типы, так что при работе с матрицами разных типов там нужно либо обмазываться макросами, либо каким-то образом постоянно передавать информацию о типах и забыть и типобезопасности. А насчёт удобства — когда нужная библиотека добавляется добавлением одной строчки в конфиг проекта, разница между стандартной библиотекой и сторонней уже перестаёт быть столь существенной.
Распараллеливание операций над матрицей с заменой по сравнению с однопоточным вариантом одной строки с гарантией отсутствия проблем с многопоточностью годится?
Стандартную библиотеку тоже непонятно кто писал, но этим людям вы почему-то доверяете.
AAngstrom
Шаблонные типы — это, безусловно, добавляет плюс в копилку rust'а, но они уже есть в C++, что сильно снижает мотивацию для переезда.
Не так всё просто. По поводу библиотек — ниже.
Это как раз то, что реализуется на уровне библиотек. В частности, подобные библиотеки уже очень давно есть и для C и для Фортрана (например, тот же, MKL). К тому же в контексте HPC чаще используется распараллеливание по распределённым процессам (используя MPI), а не по потокам. Хотя можно (но непросто) делать и то, и другое вместе.
По поводу библиотек: самое главное отличие стандартной библиотеки заключается в том, что её API задан неким стандартом. Это значит, что он (интерфейс) не будет меняться по велению левой пятки его создателя. А если и будет меняться, то это будет делаться в рамках последовательного процесса (standard -> obsolete -> deleted), растянутого на годы, а иногда и десятилетия. Подобная консервативность — это именно то, что ставит древние языки на первое место при выборе инструментов для долгосрочного проекта.
Сразу замечу: всё это совершенно не означает, что нужно теперь всю жизнь сидеть на Fortran/C++, игнорируя прогресс в языкостроении. В некотором смысле, я уже давно жду "ту молодую шпану", которая сметёт этих старичков. Но если посмотреть на то, что находится в разработке на данный момент, то для численных расчётов выбор, скорее, падёт на что-нибудь типа julia, чем на rust. Достаточно взглянуть на базовую библиотеку (Base) julia, с огромным количеством матфункций (которые, кстати, работают и с комплексными числами, что важно), с кучей операций над массивами (они, похоже, собрали всё, что есть в fortran'e и matlab'e на этот счёт), чтобы понять, что этот язык делали с прицелом на расчёты. Я не уверен, что даже в сторонних библиотеках rust'а найдётся большая часть того функционала, который уже сейчас есть у julia.
chersanya
Если я правильно посмотрел, там по ссылке по сути заменяется обычный "map" для обхода массива на его параллельную реализацию "threaded map". Это во многих языках есть из коробки или почти из коробки — если что-то не так понял, то поясните пожалуйста.
andreyverbin
Вот каким должен быть идеальный цикл — http://cl-cookbook.sourceforge.net/loop.html :)
AnthonyMikh
А зачем это делать отдельным макросом, если это прекрасно пишется на итераторах?
Gryphon88
503, хабраэффект :( Приведите тут, пожалуйста. А макросы, имхо, для использования в чистом С.
andreyverbin
Ну я ради стеба привёл эту ссылку, просто в Common Lisp loop это целый язык, сложнее самого Lisp :)
SeApps
Можт, писали:
У нас в java так:
collection.forEach(item -> {
[..]
});
Кратко и понятно
haqreu Автор
Я ведь не зря в самом начале статьи привёл реальный кусок кода из пяти вложенных циклов. Отдельностоящий `for (int i=0; i<size; i++)` тоже пристойно читается. Так что, жава жавой, а конкретные сценария использования в нагруженном коде приветствуются!
SeApps
Ах вот оно что! Ну ладно, вот конкретный сценарий:
Нужно переделать коллекцию из Product в коллекцию из ProductDto.
List = products.stream().map(ProductDto::new);
Вроде цикл есть… но его нет. По мне, очень удобно.
Можно даже с map (ключ-значение).
haqreu Автор
Это ненагруженный пример :(
sedyh
Данная конструкция не особо полезна, т.к. все возникающие в лямбде checked исключения нужно будет дополнительно оборачивать в unchecked. Почти всегда лучше написать обычный цикл.
SeApps
Ну, если это коллекция например из orm, то исключений там быть не должно.
Akon32
'for(var item: collection) {...}' — более краткий вариант.
haqreu Автор
Хххех, ну вот появился и первый минус к статье, обоснование «не согласен с изложенным». А ведь основная мысль статьи заключается во фразе «научите меня» :)
Не согласны — предлагайте мнение / варианты! Минусовать не стесняйтесь, но мне интересны мнения!
chersanya
Казалось бы, вместо первого цикла в начале поста прямо напрашивается вынести соответствующие вещи в отдельные функции/итераторы, чтобы записывалось как-то так:
Выглядит чище и понятнее, сложнее с индексами ошибиться.
haqreu Автор
Да, именно к этому сейчас и пришли. Вы только типы забыли перед объявлением переменных.
chersanya
В посте не написано, что к этому пришли — поэтому и предложил :) Про типы — да, действительно забыл (простительно т.к. не пишу на c++), добавил auto везде.
По факту с индексами вообще редко приходится напрямую оперировать — в том числе в численных научных вычислениях, которые иногда пишу.
haqreu Автор
Я auto стараюсь не использовать в циклах (ну, помимо structural binding в enumerate/zip), т.к. бывает, что переменная целочисленная, а бывает, что и ссылку возвращаю. Ну и вообще auto на один символ длиннее int :)
chersanya
Ну, из int'ов в моём варианте предполагается только iter и d, а cell, v1, v2 — это соответствующие структуры.
sedyh
В go цикл for с итератором выглядит так:
Он работает и как enumerate() и как range() в python, границы можно указать через создание слайса arr[low, high, max] либо переписав цикл в классическом виде, ненужные значения можно просто закомментировать символом «_».
LiNoofey
Да, все таки такие большие трех-, четырех- и больше мерные проходы по матрицам, массивам и т. д. стоит выносить в отдельную, с говорящим названием, функцию. Ибо я, например, сидящий на JS, как бы не крутился, все равно получаеться громоздко и неудобно (хоть через foreach() хоть через тот же for)
Nomad1
У вас конструкция из 5 вложенных циклов. Это операция такой сложности для понимания и отладки, что макросами и синтаксическим сахаром вы можете ее сделать только менее понятной для стороннего человека. У вас отличные комментарии в коде, сам он весьма читабельный. Если в первую секунду не ясно, что и как итенируется, то потом видны комментарии и все становится на свои места. Смешайте это на половину с for-range или for each и читабельность упадёт. Буквально мне не нравятся только константы 4 и пр. в цикле.
AndDav
Вместо вложенных циклов можно (но не факт что нужно) использовать cartesian_product (https://gcc.godbolt.org/z/vaasqq):
Maccimo
2020 год близится к концу, а в C++ так и не исчезла страсть к таинственным аббревиатурам. А потом студенты в порядке букв будут путаться.
mobi
Это не аббревиатура, а греческая буква (честно одолженная из языка программирования APL).
Maccimo
Посмотрел в википедии, забавно.
Была какая-нибудь публичная дискуссия на тему выбора названия функции, есть где почитать? Тот же
range
IMHO интуитивно понятнее.dataman
Ну а пока есть FunctionalPlus (C++14), range-v3 (C++14/17/20) и NanoRange (C++17).
dataman
А так for выглядит в
alexejisma
Как мне кажется, хорошим является вариант с
range
. Только, я бы вынес всё, что Вы писали внури тела функции в отдельный класс и кроме вариантаrange (end)
добавил быrange (begin, end)
и, быть может,range (begin, end, step)
. Но куда более удобным вариантом будет сделать класс шаблонным. Тогда можно будет писать, например,for (auto i : range (it1, it2))
.haqreu Автор
У меня основная мысль введения слова range в лексикон нашего проекта именно в подчёркивания обычности цикла, это строго от 0 до n-1. Поэтому у меня строго range с одним аргументом. Если делать range(begin, end, step), это проще обычный for(;;) использовать.
yeswell
Вот результаты моих экспериментов, когда-то тоже надо было много for’ов писать: github.com/yeswell/for-range
haqreu Автор
Маленькое замечание:
std::iterator
уже убрали из языка. Вместоlong long
, наверное, логичнее использоватьsize_t
.yeswell
О, про
std::iterator
не знал, но когда писал он точно былhaqreu Автор
Ну вот в частности поэтому я не очень тороплюсь переходить на >=C++20, да и из C++17 использую не всё. Что-то в последнее время комитет как с цепи сорвался.
Devilar
Довольно банальная мысль, но я против макросов, которые дублируют и так вполне понятные конструкции языка. Эти макросы хорошо понятны Вам — вы их придумали и вы ими пользуетесь. Но для нового человека это доп нагрузка, нужно их выучить, и если код с for или range он прочитал бы сходу просто владея синтаксисом языка, то макросы придется дополнительно изучить и запомнить.
haqreu Автор
Именно поэтому я написал эту обёртку с range; но справедливости ради любой нормальный IDE (MS Visual Studio, например) при наведении мышки на FOR(i, n) покажет определение дефайна.
Конкретно наш код будет читаться только членами нашей команды, поэтому локальные загогулины допустимы, хотя и не очень желательны.
Devilar
Состав команды штука не постоянная, наводить на макросы и читать определение дополнительное обременение. Лично я для себя осознал, что это действительно ненужное и усложнение, когда у нас практику студенты проходили. Они знали язык, но макросы сбивали их столку и заставляли задать лишь один вопрос — «а зачем?!», и когда я пытался объяснить по их глазам было видно что они не совсем согласны со мной. Плюс введение макроса это не только усложнение в чтении, но и в написании, теперь надо им везде пользоваться, помнить его. И обычно количество таких макросов имеет свойство расти.
Я не против макросов в целом, я против их использования в тех местах, где синтаксис языка и так неплохо справляется.
bfDeveloper
Выше уже упоминали D, а если вспомнить, что все эти ranges в C++ попали оттуда (именно в виде шаблонных конструкций этапа компиляции), то и for надо смотреть там же.
В C++ это переносится примерно так же, как у вас в статье
Я считаю, что синтаксический сахар в виде range-based for вместе с хорошей библиотекой ranges (ranges-v3, которая фактически в стандарте C++20), и есть идеальный for.
Ну и стоит упомянуть, что функциональщина в виде map, filter или cartesianProduct часто лучше читается. Можете посмотреть на ndslice и увидеть, как хорошо решается задача многомерного прохода на тех же абстракциях.
nin-jin
В D, кстати, тоже есть
iota
:bfDeveloper
Да, но 0..100 читается лучше. iota хороша для функциональных цепочек с map, filter, chunk и тд, и ещё для шага отличного от 1, он там третьим аргументом передаётся.
Имхо, это недоработка в языке, что 0..100 это только для foreach. Было бы лучше, если можно было бы в любом месте использовать для обозначения диапазонов. Вместо этого… переиспользовали для срезов и значение там совсем другое.
Chronicler
Самый лучший foreach что я видел — у Kotlin
Akon32
На scala аналогом будет
Chronicler
А если нам нужно использовать каждый элемент несколько раз?
Akon32
Пожалуйста:
list foreach { it=>
action1(it)
action2(it)
}
Вложенные циклы так даже удобнее:
list1 foreach { a=>
list2 foreach { b=>
use(a, b)
}
}
Но, конечно, второй случай можно записать более традиционно:
for(a<-list1; b<-list2) {...}
Kotlin задумывался как более простая scala. it-синтаксис вообще из groovy.
Chronicler
Так то ваш пример в kotlin не сильно длиннее будет
chersanya
Чем этот forEach удобнее котлиновского же цикла for, да и цикла for в других языках?
vs
AnthonyMikh
Такой метод удобнее, когда цепочка адаптеров итераторов уже весьма длинная.
folkner
А почему бы, в Вашем конкретном случае, не запускать вычислания по каждому из направлений параллельно и потом индексиванно сливать их вместе. Понимаю, что немного оффтоп, но по-моему, нужно просто немного оптимизировать алгоритм.
anatolii_kostrygin
В питоне часть проблем со сложенными или вложенными циклами неплохо решает библиотека itertools (там ещё много других полезных итераторов):
Чёрт его знает, как такую штуку реализовать на C/C++, но раз тут мозговой штурм…
Ещё если в проекте много повторяющихся геометрических итераций вида тетраэдры->вершины->грани, то можно заранее создать стандартные списки индексов, по которым потом итерироваться — такой кэшированный результат itertools.product, что снимет проблемы с производительностью.
NN1
Когда-то давно этим баловался http://rsdn.org/forum/src/1560894.1
Практика в те годы показала что добиться аналогичной генерации не так легко.
Кроме того такой код менее понятен другим.
Av1chem
Очень редко пишу вложенные циклы. Только если вложенный итератор от внешнего зависит. В остальных случаях, естественно, for I, j, k in itertools.product().