Pull to refresh

Comments 173

Во втором питоне был xrange (итератор). В целом, for in — это один из элементов хорошей эргономики языков, который появился относительно поздно.


В Rust его эргономику подняли ещё выше. Не теряя общности протокола итерации, они сделали так:


for x in 0..10 {
   ...
}

Более того, зацените с inclusive:


for x in 0..=10 {
   ...
}

(досчитает до 10 включительно)


А ещё много всего вкусного: https://doc.rust-lang.org/reference/expressions/range-expr.html

Поэтому я и смотрю в сторону range for, но мне важно понять, как писать понятные циклы именно на цпп, т.к. вопрос перехода на что-то другое у нас не встанет ещё долго.
Prelude> data Vec3 = Vec3 {x, y, z::Double} deriving Show
Prelude> points = [Vec3 6 5 8, Vec3 1 2 3, Vec3 7 3 7]
Prelude> mapM_ print  points

Пример с выводом X в Haskell.
ЗЫ: Только я не настоящий сварщик.
ЗЫ: запускал в интерпретаторе ghci

как писать понятные циклы именно на цпп

наверно отказываться от них где только возможно.
цикл в общем случае заменяется на свертку, но есть куча алгоритмов для частных случаев.
Вот тут хорошая статья об этом

Это мы еще не вспоминали (0..10).map(function)

Как мне кажется, в используемом автором примере интереснее всего использовать zip()
Скрытый текст
for (point, weight) in points.iter()
                             .zip(weights)
{
    println!("{}, {}", point.x, weight);
}

И для плюсов, невзирая на отсутствие в стандартной библиотеке он реализуется достаточно просто (сто лет кресты не использовал, так-что легкий копи-паст кода haqreu).
А вот за zip спасибо! Забыл про него. Надо подумать, как бы поэлегантнее его сделать с переменным количеством аргументов (впрочем, сильно вряд ли кто-то захочет зипить больше трёх массивов, уж больно громоздкая запись будет).
Да, я уже посмотрел. В целом стараюсь стоять от буста подальше, хотя некоторые вещи оттуда можно перенять.

В хаскелле есть буквально zip/zip3/zip4, а больше обычно и правда не нужно.

В целом, for in — это один из элементов хорошей эргономики языков, который появился относительно поздно.
В Rust его эргономику подняли ещё выше. Не теряя общности протокола итерации, они сделали так:
for x in 0..10

Эргномика выше по сравнению с чем? Во многих языках такой синтаксис есть, с разными мелкими отличиями типа : вместо ... Да даже в питоне, который намного старше раста, выглядит похоже: for x in range(10), или for x in range(0, 10) если хотите нижнюю границу указывать.

Так в первом комментарии было написано мол "вот есть обычный for-in, а в расте ещё эргономичнее его сделали". Вы же сравниваете не с for-in циклом.


Ну а сам for-in-range в питоне уже лет 20 существует, если не больше — думаю, даже это странно считать "относительно поздно" в плане развития языков программирования. Уверен, что в каких-то других языках такое и раньше было сделано.

while/for в языках появился примерно на 20 лет раньше питона, насколько я понимаю. То есть я говорю, что конструкция for in появилась существенно позже, чем обычный for, то есть это относительно новое улучшение эргономики.

Эргономика выше за счёт добавления range в синтаксис языка. 2..4 явно выразительнее, чем range(2,4). Букв меньше, скобок меньше.

В Rust лучше использовать итераторы. Одновременно и читабельнее и быстрее. Не лишним будет упомянуть rayon (docs.rs). Многопоточные числодропилки в пару строчек можно делать:


use rayon::prelude::*;

fn sum_of_squares(input: &[i32]) -> i32 {
    input.par_iter()
         .map(|i| i * i)
         .sum()
}

Очень "интересная" логика. По умолчанию, если выражаться математикой цикл [0,10). В расте об эргономике вообще не думают? Обычно сахар в виде ".." подразумевает полное включение от и до

Думают, думают.


В большинстве случаев надо считать от нуля до N-1. Даже у автора поста именно этот случай описан.


Причина — если у вас есть N элементов, то у них номера от нуля до N-1.


Понятно, что математику непривычно. Но математику так же не привычно, что в компьютере нет ни натуральных, ни вещественных чисел. А ещё нельзя досчитать до бесконечности (в обоих направлениях).

Во-первых, откуда "обычно", во-вторых, на практике полувключающие диапазоны удобнее

Посмотрел таки ради интереса по вашей ссылке — эта штука с двумя точками в расте очень ограниченная, ничего не поменялось с тех пор как я с ним игрался:


  • Произвольный шаг в таком синтаксисе не поддерживается, для него нужно наворачивать (0..10).stepby(2).
  • Можно использовать только со стандартными целыми числами, если я правильно понял: никаких вам bigint/float/datetime/… .
  • Произвольный шаг в таком синтаксисе не поддерживается, для него нужно наворачивать (0..10).stepby(2).

С учётом того, что step_by работает не только на численных диапазонах, а на произвольных итераторах, не вижу, в чём проблема.


  • Можно использовать только со стандартными целыми числами, если я правильно понял: никаких вам bigint/float/datetime/…

После стабилизации Step можно будет реализовать его для своих типов и использовать для нативных диапазонов. Если nightly не пугает, то можно и сейчас.

С учётом того, что step_by работает не только на численных диапазонах, а на произвольных итераторах, не вижу, в чём проблема.

Ну так соответствующая операция на итераторах в любом языке наверное есть, в котором итераторы используются. Даже в питоне — islice ровно это делает.
В общем, никак не понимаю чем итераторы именно в расте удобнее/эргономичнее, как тут выше писали — по-моему они совершенно обычные, возможности такие же как везде.


После стабилизации Step можно будет реализовать его для своих типов и использовать для нативных диапазонов.

О, это хорошо, рад за раст :)


Если nightly не пугает, то можно и сейчас.

Я растом по факту не пользуюсь, только игрался и посматриваю иногда на концептуальном уровне. Лично для меня он встал в категорию языков "придуманы и реализованы интересные/полезные идеи, но мне не подходит" — там же и например lisp, haskell. Мне для всяких вычислений, статистики и анализа данных (+ остальное по мелочи) главное — экосистема, которая в расте по сути отсутствует; ещё слишком многословно выглядит всё-таки, и слишком "низкоуровневый".

Почему вы не пошли с for дальше — в большинстве случаев вам индекс не нужен, вам нужно перечисление элементов. То есть следующий этап foreach.
Вы знаете, в большинстве случаев у меня даже основного массива элементов нет. Например, для конкретного дома номер 5 по Садовой улице у меня есть массив имён ответственных квартиросъёмщиков, есть булевский массив (того же размера) наличия в квартире счётчика холодной воды, целочисленный массив (опять же того же размера) задолженности по оплате лифта и тому подобное, то есть, куча атрибутов для каждой квартиры. А вот непосредственно массива квартир у меня нет, они неявно присутствуют в индексе массивов. Поэтому `std::for_each` для меня тупиковый путь. Мне нужна возможность получить доступ ко всем данным для каждой взятой квартиры.

Звучит как zip() + filter() или даже filter_map(). С хорошей базой итераторов можно много делов наворотить.

Вам скорее нужна правильная абстракция, позволяющая работать со структурой массивов, как с массивом структур.

Да, возможно. Есть идеи помимо зипа?

Специальный контейнер, возвращающий прокси, которые берут данные из массивов по заданному смещению.

Учитывая, что у меня массивов каждый раз разное количество и каждый разного типа, я пока вижу только зип, который возвращает std::tuple для данного смещения…

Лучше не tuple, а namedtuple (есть такое в C++?), или struct — чтобы обращаться нормально по имени, типа a[i, j].cost. Не скажу за C++, но я иногда пользуюсь таким виртуальным представлением struct-of-arrays в виде array-of-structs — удобно, когда работающая с ним функция особо не заморачивается, какой из этих двух вариантов ей передали.

tuple можно легко разобрать на запчасти при помощи structural binding, так что имена будут красивыми.

Только если типы в кортеже одинаковые, то их легко перепутать

Ну, если запись вида


std::vector<int>  width(n);
std::vector<int> height(n);
[...]
for (auto [w,h] : zip(width, height)) {
     [...]
}

то перепутать сложно. А вот если объявление кортежа дальше, то да, можно перепутать. Но тут мы говорим именно о таком локальном объявлении.

Я имел в виду какой-то такой сценарий, в котором обычно пользуюсь SoA-as-AoS. Есть функция, которая обрабатывает массив структур:


f(A) = [a.field_1 + a.field_2 for a in A]

Ещё есть функция, которая работает с обычными числовыми массивами:


g(A, B) = A * B

И нужно несколько раз по очереди их применять к данным одного и того же массива. Тогда пишу примерно так:


field_1 = ... # массив чисел
field_2 = ... # массив чисел
A_s = StructArray(field_1=field_1, field_2=field_2)

for iter in 1:niters
    xxx = f(A_s)
    yyy = g(A_s.field_1, A_s.field_2)
end

Тут важно, чтобы элементы A_s представлялись как структуры с правильными названиями полей, иначе f(A_s) не сработает.


Вообще очень удобно получается такая абстракция — код чище, функция может принимать один массив вместо условных пяти, которые должны быть одного размера.

Лучше всего со вложенными циклами дела обстоят у scala, в одной конструкции for делаются все уровни:

for (a <- 0 to 4; b <- 1 to 5; c <- 'a' to 'c') {
  println(s"$a, $b, $c")
}


output:

0, 1, a
0, 1, b
0, 1, c
0, 2, a
0, 2, b
0, 2, c

Я скалу не знаю, но глядя на этот синтаксис у меня есть сразу же куча интерпретаций:


  1. Одновременная итерация по трём итерейбл (типа for x in zip()) до момента пока все итерируются.
  2. Одновременная итерация по трём итерейбл (типа for x in zip()) до момента пока хотя бы одна итерируется.
  3. Три вложенных цикла (слева направо)
  4. Три вложенных цикла (справа налево).

Так что не очень выразительно.

Главное ни в коем случае не смотрите на output под кодом, а то неинтересно когда вместо четырех интерпретаций остается только одна.
Вы знаете, но если нужно смотреть на вывод для того, чтобы понять, что делает кусок кода, это тревожный звоночек. Впрочем, scala и rust это интересно, но меня интересует как улучшить читаемость кода именно на C++ в рамках уже существующих стандартов. Хотя, конечно, примеры из параллельных вселенных интересно изучить, вдруг что можно перенять…
Вы просто написали что подглядываете за соседями, я вас познакомил с еще одним соседом. Синтаксис может казаться непривычным, с первого взгляда код скалы и вправду понять непросто, но есть такое расхожее мнение — кто начинает писать на скале, на других языках писать больше не хотят. Так что это просто пример, согласитесь что
for (a <- 0 to 3; b <- 1 to 4; c <- 2 to 5)
выглядит куда компактней, чем
for (int a = 0; a < 4; a++) {
  for (int b = 1; a < 5; b++) {  
    for (int с = 2; a < 6; c++) {  


По крайней мере это выглядит любопытно
Может оно и выглядит любопытно, но мне кажется всё же нечитаемым. По крайней мере, мне нужно конкретно щуриться и выделять текст блоками, чтобы понять, что происходит в этом цикле. Кроме того, не забывайте, что в реальном коде не будет простых границ как в вашем примере, это будут другие переменные/константы/вызовы функций, что читаемости не прибавит никак.

В сях тоже можно при желании вложенные циклы сделать в рамках одной конструкции for(;;). Но это как с тем троллейбусом: зачем?
На самом деле там 3 конструкции, for не более чем сахар и написать можно еще так:

for {
  a <- 0 to 3
  b <- 1 to 4
  c <- 2 to 5
} {...}
О, так читается гораздо лучше. Но я хочу идти ещё дальше, я хочу, чтобы самый частый цикл не имел даже упоминания о нижней границе. Только имя переменной и верхняя граница. Именно поэтому в моей имплементации `range(int n)` имеет только один аргумент, а не три, как в питоне. Если вдруг мне понадобится пробежать не 0..n-1, у меня будет цикл другого вида, без слова `range`, и это привлечёт моё внимание.

Сделал на JS, получилось в 30 раз медленнее вложенных циклов


for(const [x, y, z] of matrix(100, 100, 100)) {
    console.log(x, y, z)
}

Исходный код
function* matrix() {
  const len = arguments.length,
        values = Array(len).fill(0)

  for(let pos = len - 1;;) {
    while(values[pos] < arguments[pos]) {
      yield values.slice()
      values[pos]++
    }
    while(pos-- > 0 && values[pos] + 1 === arguments[pos]);
    if(pos < 0) break
    values[pos]++
    while(++pos < len) values[pos] = 0
    yield values.slice()
  }
}

const a = 100

let time1 = -performance.now()
let res1 = 0
// for(let i = 0; i < 10000; i++)
  for(const [x, y, z] of matrix(a, a, a)) {
    res1 += x + y + z
  }
time1 += performance.now()

let time2 = -performance.now()
let res2 = 0
// for(let i = 0; i < 10000; i++)
  for(let x = 0; x < a; x++) {
    for(let y = 0; y < a; y++) {
      for(let z = 0; z < a; z++) {
        res2 += x + y + z
      } 
    } 
  }
time2 += performance.now()

console.log(res1 === res2, res1) // true 148500000
console.log('Matrix:', time1, 'ms') // "Matrix:" 925.7049999978335 "ms"
console.log('Loop:', time2, 'ms') // "Loop:" 30.43500000057975 "ms"
console.log(time1 / time2) // 30.32500790616872

Ну, это же JS, тут не бывает zero-cost абстракций.

А где такое можно провернуть с zero-cost? Не представляю, как здесь constexpr использовать.
Как самый очевидный (для меня) пример zero-cost приема, ведь выражение с constexpr не вычисляется в рантайме.
Но я хочу идти ещё дальше, я хочу, чтобы самый частый цикл не имел даже упоминания о нижней границе.

Можно создать implicit класс для чисел или любого другого типа, и потом писать что-то вроде for (i<-5.times){...} или 5 times { .... }.
Но более привычно выглядят варианты for(i<-0 until 5){...} и for(i<- array.indices){ ... }.

а как на скале написать с++ аналог:

for(int = i=0,j=0; i<5;i++,j++) — цикл один, а переменных несколько?
как не спутать с тем что в примере с вложенными.

По-хорошему такой цикл через zip делается, хоть на скале хоть на чём: for (i, j) in zip(0:4, 0:4) с поправками на детали синтаксиса разных языков.

Как-то так (можно и в одну строчку свернуть):
for {
  j =  0
  i <- 0 to 5
  j += 1
}


Но, с большой вероятностью, за такой код в scala сообществе закидают тапками )
оно вам видится нечитаемым потому что вы не знакомы с этой парадигмой. если привыкнете ней — этот код будет намного более читаемым, и щуриться не надо.К тому же короче, что тоже помогает чтению при прочих равных.
b < — 1 to 4;

С одной стороны, визуально кажется что там отрицательное значение -1
for (int b = 1; a < 5; b++) {

С другой стороны, возможен баг за счет копипасты

for в скала это обычная do-нотация, о чем можно легко догадаться по <-, следовательно вспоминаем как реализован стандартный аппликатив для списков — каждый с каждым.

Для более полной демонстрации циклов scala я бы привёл пример посложнее:


for(i<-0 until 10;
    k=h(i);
    j<-1 to 5 if f(j,k)>5; 
    _ = println(s"$i $j $k"); 
    (v,5)<-someIterable){
   ...
}

И это ещё без yield (который в scala довольно убогий по использованию и производительности).

/ sarcasm
В 2020 должна быть функциональщина с хвостовой рекурсией вместо цикла
/


Ну а если серьезно — зависит от требований и контекста. Если не нужна оптимальная производительность, то можно действительно использовать функциональщину или что-то типа LINQ из С# аля method chaining (не знаю, завезли ли такой синтаксис в плюсы уже наконец-то). Если производительность таки важна, можно попробовать вынести циклы выше 3-го уровня вложенности в отдельную функцию, в надежде что умный компилятор все заинлайнит. Желательно вынести так чтоб это ещё и имело какой-то «физический смысл», тобишь было очевиден новичку в проекте.


Ну и да, я лично целиком поддерживаю ваше желание убрать «лишние буквы» — чем меньше букв тем проще читать код (обычно, бывают исключения).

Мне крайне важна производительность, мы пишем конкурентноспособные числодробилки, так что любая абстракция пойдёт до тех пор, пока компилятор её способен воплотить при нулевой стоимости по сравнению с обычным си. Разумеется, мы выносим внутренности в отдельные функции, но это не всегда возможно.

Ложная дилемма. В расте итераторы быстрее циклов. Потому что итераторы автоматически выкидывают все проверки индексов, например.


То что LINQ в шарпе реализован через одно место не означает, что нельзя делать быстрые и эффективные итераторы.

Я в общем-то имел в виду не то что LINQ медленный, а то, что переход на функциональщину (генераторы, иммутабельность, алгебраические типы, и т.п.) может при правильном применении упростить код, но (обычно) негативно сказывается на производительности, т.к. у оптимизатора появляется сильно больше работы. Ну и синтаксис С++ не позволит эти вещи использовать «удобно» (в том же расте тут все сильно лучше). Опять же, оптимизатор, возможно, сможет перелопатить функциональщину, но это нужно проверять «отдельно» — за читабельность и удобство отладки приходится платить.


Ну а что касается LINQ в шарпе — с ним не все так плохо — за ~20% замедления код получается намного более читабельным. К тому же, LINQ в шарпе реализует генераторы, а не итераторы/ренжи — по сути, это монада List из хаскелл — многие операции LINQ позволяют работать с “бесконечными” последовательностями. Исключение составляют операции требующие полной материализации (например, OrderBy). Соответственно, LINQ реализован поверх ко-процедур, а не поверх итераторов, что и даёт накладные расходы (точнее, усложняет работу оптимизатора).

Ну сопрограммы в что-то такое и компилируются :) Именно это я и имею в виду под сложностями для оптимизатора — ему теперь надо понимать что это временный объект чтоб полностью убрать его из GC (не уверен что оптимизатор вообще такое умеет)

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

Это как раз наоборот. При использовании ФП примитивов у оптимизиатора больше возможностей, а не меньше. Классический пример, в обычном цикле for компилятор обязан проверять выход за границы массива. При этом при использовании foreach на итераторе он знает, что выхода за границы не будет и можно их выкинуть. С декларативным описанием программист сообщает больше информации о том, что он хочет получить в итоге, а не как, поэтому компилятору дается больше свободы в реорганизации кода чтобы получить нужый результат.


Ну а что касается LINQ в шарпе — с ним не все так плохо — за ~20% замедления код получается намного более читабельным.

Только не 20% — а на порядок


img

В .NET 7 наконец это гораздо улучшили: ..NET 7 improvements

В некоторых местах автовекторизация LINQ выдаёт лучшую производительность чем цикл for.

Добавляем Where и автовекторизация рассыпается, накладные расходы же на виртуальные вызовы не пропадают никуда.

На КПДВ узел под названием «совершенная петля» (perfection loop).

прошу ногами сильно не бить, но «совершенная петля» это «perfect loop», а «perfection loop» это «петля совершенства».
А чего ногами-то бить. Я привёл названия (причём со ссылкой) на двух языках, переводил не я. И я не уверен, что в русский язык название пришло из английского, а не, скажем, голландского…

"perfection" в данном случае неоднозначно — можно перевести как "петля совершенствования", c учётом контекста статьи очень даже интересная игра слов получается.

Мне одному кажется, что если значения можно разместить в массивах, то самое удобное оформление вложенных циклов реализовано в фортране? Например:

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
Я не уверен, что это самое удобное. Кроме того, мне нужно понять, как жить дальше именно c C++ :)
по старинке жить надо. какой смысл в новомодных извращениях если они не дают выигрыша в скорости судя по асму. попытки сокращения, внесения всяких символов и повышения смысловой нагрузки в конструкциях языка несет только одно — ухудшение читаемости и понимаемости кода обычным человеком. тупо повышается порог вхождения в профессию и стоимость услуг. изначально смысл появления языков высокого уровня был в снижении этого порога.
Выиграть в скорости у обычного for(;;) вряд ли удастся. А с утверждением, что любая попытка ввести стиль программирования приводит к ухудшению читаемости, я не согласен категорически. Вы примеры из статьи смотрели?
естественно я всю статью прочитал. я про стили ничего не говорил. у меня стиль вообще своеобразный тк я все время был одиночкой и форматирую свой текст для своего удобства. {}=>[]<=() когда этого становиться много в языке это зло. но я начинал с паскаля на см-4… и предпочитаю легко читать и писать код, а не разгадывать символьные ребусы и запоминать вычурные конструкции языка.
Вы о каких вычурных конструкциях-то? У меня нет предложения реорганизовать рабкрин (простите, стандарт c++). У меня есть конкретный вопрос о том, как писать на современном c++ таким образом, чтобы это экономило усилия разработчика в рамках одной команды. То есть, о стиле программирования в рамках одного проекта. У нас текучка небольшая, и мне несложно объяснить стиль каждому новому человеку.
мне непонятно зачем читать эти циклы. почему они не в функциях с нормальным говорящим названием.
Если мне нужно пробежать все ячейки кубической сетки, то у меня по-любому будет три вложенных цикла, будь они разнесены по функциям или нет.
ну и зачем их читать. оформили в функцию с параметрами и читайте эти параметры в одной строчке. по мне так это вопрос уровня создания меню. пишется и вызывается MakeMenu(). А уже MakeMenu совершенно тупая функция в которой куча InsertItem(). ну или я чего-то не понимаю.
Оформление для грядущих поколений это хорошо. Но для начала код нужно написать и отладить. И я вас уверяю, во время отладки этот код читается много-много-много раз.

Не обязательно три вложенных цикла — можно по массиву этих ячеек в линейном порядке проходить. Если проходимая структура более сложная, чем многомерный массив, то казалось бы можно один раз реализовать функцию обхода по ней с индексом, а потом эту функцию только использовать как аргумент в for-in. Или у вас такое решение по какой-то причине не проходит?

Давайте забудем про три вложенных, скажем, мне нужно два вложенных для обработки изображения. Да, я могу пройтись одним линейным циклом по всем пикселям изображения, но если мне нужно каждый пиксель усреднить с его четырьмя соседями, то мне нужны индексы (i,j) для каждого пикселя. Да, я смогу их получить целочисленным делением и взятием по модулю из линейного индекса, но насколько это оправдано?

По-моему такой сценарий абсолютно типичный и не требует написания вложенных циклов по размерностям каждый раз. Один раз реализуете функцию обхода-по-индексам если её нет, и потом эту функцию используете. Я бы примерно так сделал (пример на джулии, т.к. в современном c++ почти не ориентируюсь; но код не завязан фундаментально на язык):


A = ... # наш массив
offsets = CartesianIndex.([(-1, 0), (1, 0), (0, -1), (0, 1)])
for I in CartesianIndices(A)
    cnt = 1
    for off in offsets
        if checkbounds(Bool, A, I + off)  # для примера игнорируем "соседей" вне границ массива
            cnt += 1
            A[I] += A[I + off]
        end
    end
    A[I] /= cnt
end

При этом, как видно из кода, очень легко обобщить на произвольное понимание "соседей" клетки, на многомерные массивы, и т.п. — явно удобнее, чем вложенный цикл на каждую размерность массива, и захардкоженные координаты соседей.

как жить дальше именно c C++

Зачем вам этот дедушка с его неизлечимыми проблемами?

Простите, а какие варианты? Раст?
Дмитрий $mol наверное предложит

$mol он для веба предлагает, а тут он предложит D (это язык такой, не смайлик)

Запись вложенных циклов одной строкой — согласен, удобная фича, тоже часто пользуюсь. Но почти больно :) смотреть на повторение i,j,k трижды в вашем блоке кода: иногда попадался на опечатках в таких случаях, особенно если индексы массива из переменных цикла получаются сложнее (типа a[i + 2, j, k * 3]), или идут в другом порядке, или надо будет потом подправить это место. Поэтому теперь стараюсь писать так (пример на julia), т.е. не повторяя много раз получение индекса массива:


for i in 1:10, j in 2:2:10, k in 6:8
    I = CartesianIndex(i, j, k) 
    if b[I] == 0 continue end
    a[I] = 3 * i / b[I] + c[j ÷ 2]
end
Мне одному кажется, что если значения можно разместить в массивах, то самое удобное оформление вложенных циклов реализовано в фортране? Например:

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

Но ведь от этого отказываются?
The FORALL construct is an obsolescent language feature in Fortran 2018.

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?
Сложные расчёты всё равно не для gcc, а ICC/IFC, Nvidia/PGI, IBM, Cray и прочие.

Это вы сильно загнули. Опыт HPC с gcc имеете?

Мы не делаем местечкового стандарта, а думаем о стиле оформления проекта, а это в любой серьёзной конторе есть.
Я не уверен, что это самое удобное. Кроме того, мне нужно понять, как жить дальше именно c C++ :)

В статье был намек на соседей… это просто пример. Причем из языка, который достаточно широко используется для моделирования, и по эффективности мало кому уступает. Что позволяет писать вычислительную часть на фортране и цеплять эти функции к проектам на других языках. Но у С++ эффективность сравнима с фортраном (если сравнивать хорошие компиляторы), поэтому переписывать уже имеющийся проект смысла нет, разумеется. Особенно с учетом отсутствия разработчиков, владеющих этим хорошим, но очень уж узконишевым языком…
А вопрос удобства всегда имеет субъективный оттенок, естественно. Но массивы и их сечения в современном фортране действительно очень мощные. А элементы ООП, которые в него тоже недавно (лет 20 назад) добавили, позволяют делать большие проекты с хорошо читаемым кодом. Если, конечно, там нет унаследованных из древности простыней на фортране-66 с вычисляемыми GOTO и прочими подобными ужасами ;-). Которые хотя и признаны устаревшими (и категорически не рекомендуются к использованию), но до сих пор поддерживаются компиляторами «для совместимости»…
Фортран хорошая штука, и даже для наших целей (матмоделирование), казалось бы, должен подходить. Но при ближайшем рассмотрении оказывается, что (современный) C++ гораздо удобнее (нам) даже несмотря на то, что мы отказались от сложных динамических структур данных и всё упаковываем в большие массивы.
C++ гораздо удобнее (нам)

Если можно, было бы интересно узнать — по каким критериям удобнее?
Вы знаете, если отвечать предельно честно, то самый главный критерий это то, что практически никто не знает современного фортрана (например, стандарт 2018го года). Ну а вокруг этого можно навернуть всяких рассуждений о гибкости C++ и тому подобного. Лично мне удобно, что абсолютно один и тот же код может исполняться как на CPU, так и на GPU при помощи препроцессора (си/куда).

Как я и говорил, мы отказались от сложных структур данных, и все реальные данные упакованы в std::vector<>. Но поверх этих векторов у нас есть zero cost abstractions, которые облегчают разработку. Насколько я знаю (внимание, я не специалист), всякие shared pointer возможны в фортране, но это нужно конкретно над ними работать. В цпп они доступны из коробки. Так что гибкость я бы не сбрасывал со счетов.

Мне очень интересно, что вы думаете об областях примения фортрана и цпп.
К сожалению, я здесь могу только спрашивать, а не отвечать, так как являюсь типичным динозавром, давно закостеневшим в развитии, и не вымершим вовремя только в силу какого-то недоразумения. Поэтому мой взгляд на этот вопрос совершенно односторонний…
Если подробно, то мы уже 30 лет пилим очень узконишевую программу для научных исследований, причем основная часть сделана на фортране, а интерфейс — на плюсах. Я сам программирую только на фортране, за плюсы отвечает другой человек (и его код я понимаю ровно настолько, чтобы вносить туда совершенно косметические изменения вроде исправления опечаток в комментах ;-)).
Но что касается именно перемалывания чисел, то у фортрана это получается очень даже неплохо. Второй важный нюанс — весь фортрановский код, написанный нами лет 20 назад, до сих пор не только работает, но и при необходимости модифицируется с расширением функционала, и это не требует сверхусилий. Хотя наверно это зависит не только от языка… но для данного конкретного применения фортран вполне адекватен.
Наверное, я не умею готовить фортран, но лично у меня гибкая и расширяемая база кода получается именно на цпп. Перегрузка операторов, шаблоны и прочая фигня мне сильно помогает. Компиляторы C++ наконец-то догнали фортрановские, и теперь аргумент скорости уже не работает. Наверное, всё же очень сильно зависит от того, что нужно писать. Я наш цпп код спокойно запускаю в браузере (emscripten), и мне это часто нужно. У фортрана с этим напряжёнка. Так что, чистые конечные элементы на фортране хорошо, но вокруг этого обвязки на цпп вряд ли удастся избежать. В итоге мы и FEM делаем на чистом C++.

Но повторюсь ещё раз: очень мало людей знакомы с современным фортраном, можно даже сказать что это молодой язык, как бы парадоксально это ни звучало.
Перегрузка операторов, шаблоны и прочая фигня мне сильно помогает.
В принципе, из всех подобных фишек в Фортране отсутствуют только шаблоны. Вместо них есть лишь параметризация типов — это частный случай шаблонов.
даже несмотря на то, что мы отказались от сложных динамических структур данных и всё упаковываем в большие массивы.
если вам часто приходится итерироваться по многомерным массивам, лучше напишите нормальный многомерный массив, по которому можно итерироваться через range-based for, и используйте его:
for (auto& row : arr) {
    for (auto& x : row) {
        ...
    }
}
Нет, у нас все массивы одномерные. Ну, всякие разреженные матрицы не считаем за двумерный массив :)
Можно использовать прокси-объекты, которые будут ссылаться на строки в одномерном массиве. Правда есть накладные расходы на созданеи такого объекта

Отвечу как человек, которому приходится много кодить на Фортране. (При этом не могу сказать, что являюсь большим фанатом этого языка. Почему — дальше по тексту). У Фортрана есть много преимуществ для численных расчётов, плюс язык вышел из комы, в которой он находился в 90-х, и на данный момент существует более-менее постоянная комиссия, которая занимается развитием стандарта. Но у этого стандарта есть, как минимум, две проблемы: 1). обратная совместимость требует тащить с современный язык всякие архаизмы; 2). другая проблема, характерная и для многих других соверменных языков, — нет какой-то чёткой цели развития. В итоге, многие новшества добавляются ad hoc, приводят к усложнению, которое не всегда окупается потенциальными преимуществами. К примеру, начавшееся со стандарта 2003 движение в сторону ООП сильно притормозилось, а использование этих черт языка в нынешнем их виде зачастую приводит к сильному проседанию производительности. И никаких улучшений в виде zero-cost abstractions в следующем стандарте 202х не планируется.


Поэтому ситуация на данный момент такова, что если у вас уже есть значительная кодовая база на Фортране (не обязательно legacy, может быть стандарт 90 или 2003), то переходить на C++ или что-то другое смысла нет (тем более, что у C++ своих подобных проблем со стандартом вагон и маленькая тележка), т.к. на современном Фортране можно относительно комфортно писать высокопроизводительный код, если смриться с некоторыми корявостями. Если же у вас проект уже написан на C++, то я бы точно не советовал переходить на Фортран. На мой взгляд, будущее обоих этих классических языков довольно туманно, поэтому это будет смена шила на мыло. Если же начинать проект с нуля, то стоит уже смотреть в сторону чего-нибудь типа Julia. К сожалению, этот язык ещё не выбрался из стадии альфа.


(Речь идёт о численных расчётах, поэтому rust, go и далее по списку — не предлагать.)

На мой взгляд, будущее обоих этих классических языков довольно туманно

Это точно. Я пишу на C/C++ уже четверть века, и чем дальше, тем больше он меня пугает. Но внятной замены пока не вижу.
В нём сложно управлять памятью, например. Нет хороших компиляторов, отладчиков, профайлеров, IDE…
В нём сложно управлять памятью, например.

А в чём сложности?


Нет хороших компиляторов,

А чем существующие плохи?


отладчиков, профайлеров, IDE…

Можете использовать ту же Visual Studio.

А чем существующие плохи?

C++ компиляторы только-только догнали фортран по производительности кода. D ещё далёк от этого.


А вообще D — это мертворожденный язык (по моему скромному мнению). Он был создан для того, чтобы «починить» ошибки дизайна C++. Проблема в том, что никто не может сказать внятно, в чём именно ошибки. И тем паче как их чинить.


Сборщик мусора — страшная штука. D захотел «починить» проблемы с указателями в C++. Но только сборщик мусора — не решение проблем с указателями...

D ещё далёк от этого.

С чего вы взяли? Там довольно крутой компилятор, позволяющий использовать разные бэкенды, в том числе и популярные у плюсовиков GCC да LLVM.


Проблема в том, что никто не может сказать внятно, в чём именно ошибки.

В C++ много кривых мест, начиная с использования препроцессора и заканчивая UB во всех местах. В любой книжке по D об этом всём рассказывается.


Но только сборщик мусора — не решение проблем с указателями...

Конечно, поэтому вас его использовать никто не заставляет.

Мне очень нравится D и очень жаль, что нет крупной корпорации, которая вкидывает газиллионы долларов в его популяризацию (надеюсь, это только пока). Но все вот эти вот @nogc и @betterC у меня вызывают легкую панику.

Почему? Аттрибуты функций — это вообще мощная штука, позволяющая выводить различные характеристики исходя из содержимого функций. И, наоборот, ограничивать поддерево вызовов нужной характеристикой. Того же shared и pure очень не хватает в других языках.

А что не так в rust с численными расчётами?

Последний раз, когда я смотрел, там, например, не было доступно "из коробки" ни комплексных чисел, ни многомерных динамических массивов с сечениями и всеми сопутствующими вещами. Новый язык без этих фич — сразу в топку. Rust не для численных расчётов создавался, в конце концов, и это влияет на вектор развития.

Я бы сказал, что отсутствие комплексных чисел вообще не аргумент. В C++ их тоже нет, однако считают на нём только в путь. И многомерных массивов нет. И сечений...

Я поэтому и написал про "новый язык". Понятно, что на C++ уже все привыкли к велосипедам. А комплексные числа там, в принципе, есть как часть стандартной бибилотеки. А с учётом стандарта C99 они ещё и частью языка являются (_Complex).

Мы же про удобство для конкретных целей говорим. Так-то на любом языке можно решать любые задачи. Хоть на vanilla C. Только библиотеки подавай.


Вообще говоря, никак не хотел обидеть растаманов, но язык же явно с другим фокусом создавался. Системное программирование, контроллеры, веб и т.д. Не вижу, какие такие преимущества он может принести для численного моделирования по сравнению с Fortran/C++, чтобы всё бросать и разбираться во всех этих тонкостях области действия, (не)изменяемости, unsafe и прочих специфических приблуд. При этом, для элементарных, с точки зрения численных расчётов, вещей уже нужно тащить даже не стандартные, а какие-то непонятно кем написанные библиотеки, API которых может внезапано поменяться или поддержка которых вообще может прекратиться.

На всякий случай поясню комментарий про API: время жизни очень большой части софта для численных расчётов должно измеряться десятилетиями. Поэтому на новомодный сырой язык мало кто перейдёт только потому, что это молодёжно. Очень, очень не зря современный фортран тащит на себе ярмо совместимости с предыдущими версиями стандарта.


Нас, например, очень напрягает то, что современный C++ убирает кучу старых фич, т.к. это ломает код.

Мы же про удобство для конкретных целей говорим. Так-то на любом языке можно решать любые задачи. Хоть на vanilla C. Только библиотеки подавай.

Не совсем. Некоторые вещи, отсутствующие в языке, невозможно восполнить библиотеками. В C, например, напрочь отсутствует обобщённые типы, так что при работе с матрицами разных типов там нужно либо обмазываться макросами, либо каким-то образом постоянно передавать информацию о типах и забыть о типобезопасности. А насчёт удобства — когда нужная библиотека добавляется добавлением одной строчки в конфиг проекта, разница между стандартной библиотекой и сторонней уже перестаёт быть столь существенной.


Не вижу, какие такие преимущества он может принести для численного моделирования по сравнению с Fortran/C++, чтобы всё бросать и разбираться во всех этих тонкостях области действия, (не)изменяемости, unsafe и прочих специфических приблуд.

Распараллеливание операций над матрицей с заменой по сравнению с однопоточным вариантом одной строки с гарантией отсутствия проблем с многопоточностью годится?


При этом, для элементарных, с точки зрения численных расчётов, вещей уже нужно тащить даже не стандартные, а какие-то непонятно кем написанные библиотеки, API которых может внезапано поменяться или поддержка которых вообще может прекратиться.

Стандартную библиотеку тоже непонятно кто писал, но этим людям вы почему-то доверяете.

Не совсем. Некоторые вещи, отсутствующие в языке, невозможно восполнить библиотеками. В C, например, напрочь отсутствует обобщённые типы, так что при работе с матрицами разных типов там нужно либо обмазываться макросами, либо каким-то образом постоянно передавать информацию о типах и забыть и типобезопасности.

Шаблонные типы — это, безусловно, добавляет плюс в копилку rust'а, но они уже есть в C++, что сильно снижает мотивацию для переезда.


А насчёт удобства — когда нужная библиотека добавляется добавлением одной строчки в конфиг проекта, разница между стандартной библиотекой и сторонней уже перестаёт быть столь существенной.

Не так всё просто. По поводу библиотек — ниже.


Распараллеливание операций над матрицей с заменой по сравнению с однопоточным вариантом одной строки с гарантией отсутствия проблем с многопоточностью годится?

Это как раз то, что реализуется на уровне библиотек. В частности, подобные библиотеки уже очень давно есть и для C и для Фортрана (например, тот же, MKL). К тому же в контексте HPC чаще используется распараллеливание по распределённым процессам (используя MPI), а не по потокам. Хотя можно (но непросто) делать и то, и другое вместе.


Стандартную библиотеку тоже непонятно кто писал, но этим людям вы почему-то доверяете.

По поводу библиотек: самое главное отличие стандартной библиотеки заключается в том, что её API задан неким стандартом. Это значит, что он (интерфейс) не будет меняться по велению левой пятки его создателя. А если и будет меняться, то это будет делаться в рамках последовательного процесса (standard -> obsolete -> deleted), растянутого на годы, а иногда и десятилетия. Подобная консервативность — это именно то, что ставит древние языки на первое место при выборе инструментов для долгосрочного проекта.


Сразу замечу: всё это совершенно не означает, что нужно теперь всю жизнь сидеть на Fortran/C++, игнорируя прогресс в языкостроении. В некотором смысле, я уже давно жду "ту молодую шпану", которая сметёт этих старичков. Но если посмотреть на то, что находится в разработке на данный момент, то для численных расчётов выбор, скорее, падёт на что-нибудь типа julia, чем на rust. Достаточно взглянуть на базовую библиотеку (Base) julia, с огромным количеством матфункций (которые, кстати, работают и с комплексными числами, что важно), с кучей операций над массивами (они, похоже, собрали всё, что есть в fortran'e и matlab'e на этот счёт), чтобы понять, что этот язык делали с прицелом на расчёты. Я не уверен, что даже в сторонних библиотеках rust'а найдётся большая часть того функционала, который уже сейчас есть у julia.

Распараллеливание операций над матрицей с заменой по сравнению с однопоточным вариантом одной строки с гарантией отсутствия проблем с многопоточностью годится?

Если я правильно посмотрел, там по ссылке по сути заменяется обычный "map" для обхода массива на его параллельную реализацию "threaded map". Это во многих языках есть из коробки или почти из коробки — если что-то не так понял, то поясните пожалуйста.

А зачем это делать отдельным макросом, если это прекрасно пишется на итераторах?

503, хабраэффект :( Приведите тут, пожалуйста. А макросы, имхо, для использования в чистом С.

Ну я ради стеба привёл эту ссылку, просто в Common Lisp loop это целый язык, сложнее самого Lisp :)


(loop
  for item in list
  for i from 1 to 10
  do (something))

(loop for i from 0 downto -10 collect i)

(loop for v being the hash-values in h using (hash-key k) ...)

(loop repeat 5
      for y = 1 then (+ x y)
      for x = 0 then y
      collect y)

(loop for i in *random*
   counting (evenp i) into evens
   counting (oddp i) into odds
   summing i into total
   maximizing i into max
   minimizing i into min
   finally (return (list min max total evens odds)))

Можт, писали:
У нас в java так:
collection.forEach(item -> {
[..]
});
Кратко и понятно

Я ведь не зря в самом начале статьи привёл реальный кусок кода из пяти вложенных циклов. Отдельностоящий `for (int i=0; i<size; i++)` тоже пристойно читается. Так что, жава жавой, а конкретные сценария использования в нагруженном коде приветствуются!

Ах вот оно что! Ну ладно, вот конкретный сценарий:


Нужно переделать коллекцию из Product в коллекцию из ProductDto.


List = products.stream().map(ProductDto::new);

Вроде цикл есть… но его нет. По мне, очень удобно.
Можно даже с map (ключ-значение).

Данная конструкция не особо полезна, т.к. все возникающие в лямбде checked исключения нужно будет дополнительно оборачивать в unchecked. Почти всегда лучше написать обычный цикл.

Ну, если это коллекция например из orm, то исключений там быть не должно.

'for(var item: collection) {...}' — более краткий вариант.

Хххех, ну вот появился и первый минус к статье, обоснование «не согласен с изложенным». А ведь основная мысль статьи заключается во фразе «научите меня» :)

Не согласны — предлагайте мнение / варианты! Минусовать не стесняйтесь, но мне интересны мнения!
Не согласны — предлагайте мнение / варианты!

Казалось бы, вместо первого цикла в начале поста прямо напрашивается вынести соответствующие вещи в отдельные функции/итераторы, чтобы записывалось как-то так:


for (auto iter: range(1, n_iters)) { // some iterative computation
    for (auto cell: mesh.cells()) // loop through all tetrahedra cells
        for (auto [v1, v2]: subsets(cell.vertices(), 2)) // for every pair of vertices
                for (auto d: range(1, 3)) { // for each of 3 dimensions
                    nlRowScaling(weight);
                    nlBegin(NL_ROW);
                    nlCoefficient(v1, d,  1);
                    nlCoefficient(v2, d, -1);
                    nlEnd(NL_ROW);
                }
    [...]
}

Выглядит чище и понятнее, сложнее с индексами ошибиться.

Да, именно к этому сейчас и пришли. Вы только типы забыли перед объявлением переменных.

В посте не написано, что к этому пришли — поэтому и предложил :) Про типы — да, действительно забыл (простительно т.к. не пишу на c++), добавил auto везде.
По факту с индексами вообще редко приходится напрямую оперировать — в том числе в численных научных вычислениях, которые иногда пишу.

Я auto стараюсь не использовать в циклах (ну, помимо structural binding в enumerate/zip), т.к. бывает, что переменная целочисленная, а бывает, что и ссылку возвращаю. Ну и вообще auto на один символ длиннее int :)

Ну, из int'ов в моём варианте предполагается только iter и d, а cell, v1, v2 — это соответствующие структуры.

В go цикл for с итератором выглядит так:

for i, v := range arr {}

Он работает и как enumerate() и как range() в python, границы можно указать через создание слайса arr[low, high, max] либо переписав цикл в классическом виде, ненужные значения можно просто закомментировать символом «_».

Да, все таки такие большие трех-, четырех- и больше мерные проходы по матрицам, массивам и т. д. стоит выносить в отдельную, с говорящим названием, функцию. Ибо я, например, сидящий на JS, как бы не крутился, все равно получаеться громоздко и неудобно (хоть через foreach() хоть через тот же for)

У вас конструкция из 5 вложенных циклов. Это операция такой сложности для понимания и отладки, что макросами и синтаксическим сахаром вы можете ее сделать только менее понятной для стороннего человека. У вас отличные комментарии в коде, сам он весьма читабельный. Если в первую секунду не ясно, что и как итенируется, то потом видны комментарии и все становится на свои места. Смешайте это на половину с for-range или for each и читабельность упадёт. Буквально мне не нравятся только константы 4 и пр. в цикле.

На ваш взгляд, каким должен быть совершенный цикл в 2020м году?
Примерно таким, как у вас в статье, только в C++20 (23) можно использовать стандартные функции вместо самописных. range -> iota (https://gcc.godbolt.org/z/d3bqYK). enumerate и zip должны появиться в C++23, так это выглядит в range-v3 (https://gcc.godbolt.org/z/9EfMj5):
#include <range/v3/view/enumerate.hpp>
#include <range/v3/view/zip.hpp>

#include <vector>

void test(std::vector<int> xs, std::vector<float> ys) {
    for (auto [x, i] : ranges::view::enumerate(xs)) {
        // ...
    }
    for (auto [x, y] : ranges::view::zip(xs, ys)) {
        // ...
    }
}
Вместо вложенных циклов можно (но не факт что нужно) использовать cartesian_product (https://gcc.godbolt.org/z/vaasqq):
void test(std::vector<int> xs, std::vector<float> ys) {
    for (auto [x, y] : ranges::view::cartesian_product(xs, ys)) {
        // ...
    }
}
iota

2020 год близится к концу, а в C++ так и не исчезла страсть к таинственным аббревиатурам. А потом студенты в порядке букв будут путаться.

Это не аббревиатура, а греческая буква (честно одолженная из языка программирования APL).

Посмотрел в википедии, забавно.
Была какая-нибудь публичная дискуссия на тему выбора названия функции, есть где почитать? Тот же range IMHO интуитивно понятнее.

А так for выглядит в
Zig
test "for basics" {
    const items = [_]i32 { 4, 5, 3, 4, 0 };
    var sum: i32 = 0;

    // For loops iterate over slices and arrays.
    for (items) |value| {
        if (value == 0) {
            continue;
        }
        sum += value;
    }
    assert(sum == 16);

    for (items[0..1]) |value| {
        sum += value;
    }
    assert(sum == 20);

    var sum2: i32 = 0;
    for (items) |value, i| {
        assert(@TypeOf(i) == usize);
        sum2 += @intCast(i32, i);
    }
    assert(sum2 == 10);
}

test "for reference" {
    var items = [_]i32 { 3, 4, 2 };

    for (items) |*value| {
        value.* += 1;
    }

    assert(items[0] == 4);
    assert(items[1] == 5);
    assert(items[2] == 3);
}

test "for else" {
    var items = [_]?i32 { 3, 4, null, 5 };

    var sum: i32 = 0;
    const result = for (items) |value| {
        if (value != null) {
            sum += value.?;
        }
    } else blk: {
        assert(sum == 12);
        break :blk sum;
    };
    assert(result == 12);
}


Замечу, что в Zig нет итераторов, но можно определить внутреннюю структуру (обычно Iterator) и c функциями next, prev и т.д. и использовать while:
var it = somevar.iterator();
while (it.next()) |value| {
...
}

В предстоящем релизе значительно улучшены compiletime-возможности (помимо прочего), так что у Zig большое будущее. :)
P.S. Используется подсветка кода С++.
Как мне кажется, хорошим является вариант с range. Только, я бы вынес всё, что Вы писали внури тела функции в отдельный класс и кроме варианта range (end) добавил бы range (begin, end) и, быть может, range (begin, end, step). Но куда более удобным вариантом будет сделать класс шаблонным. Тогда можно будет писать, например, for (auto i : range (it1, it2)).
У меня основная мысль введения слова range в лексикон нашего проекта именно в подчёркивания обычности цикла, это строго от 0 до n-1. Поэтому у меня строго range с одним аргументом. Если делать range(begin, end, step), это проще обычный for(;;) использовать.

Маленькое замечание: std::iterator уже убрали из языка. Вместо long long, наверное, логичнее использовать size_t.

О, про std::iterator не знал, но когда писал он точно был

Ну вот в частности поэтому я не очень тороплюсь переходить на >=C++20, да и из C++17 использую не всё. Что-то в последнее время комитет как с цепи сорвался.

Довольно банальная мысль, но я против макросов, которые дублируют и так вполне понятные конструкции языка. Эти макросы хорошо понятны Вам — вы их придумали и вы ими пользуетесь. Но для нового человека это доп нагрузка, нужно их выучить, и если код с for или range он прочитал бы сходу просто владея синтаксисом языка, то макросы придется дополнительно изучить и запомнить.

Именно поэтому я написал эту обёртку с range; но справедливости ради любой нормальный IDE (MS Visual Studio, например) при наведении мышки на FOR(i, n) покажет определение дефайна.


Конкретно наш код будет читаться только членами нашей команды, поэтому локальные загогулины допустимы, хотя и не очень желательны.

Состав команды штука не постоянная, наводить на макросы и читать определение дополнительное обременение. Лично я для себя осознал, что это действительно ненужное и усложнение, когда у нас практику студенты проходили. Они знали язык, но макросы сбивали их столку и заставляли задать лишь один вопрос — «а зачем?!», и когда я пытался объяснить по их глазам было видно что они не совсем согласны со мной. Плюс введение макроса это не только усложнение в чтении, но и в написании, теперь надо им везде пользоваться, помнить его. И обычно количество таких макросов имеет свойство расти.
Я не против макросов в целом, я против их использования в тех местах, где синтаксис языка и так неплохо справляется.

Выше уже упоминали D, а если вспомнить, что все эти ranges в C++ попали оттуда (именно в виде шаблонных конструкций этапа компиляции), то и for надо смотреть там же.


foreach (i; 0..100) {}

В C++ это переносится примерно так же, как у вас в статье


for (auto i : iota(100)) {}

Я считаю, что синтаксический сахар в виде range-based for вместе с хорошей библиотекой ranges (ranges-v3, которая фактически в стандарте C++20), и есть идеальный for.
Ну и стоит упомянуть, что функциональщина в виде map, filter или cartesianProduct часто лучше читается. Можете посмотреть на ndslice и увидеть, как хорошо решается задача многомерного прохода на тех же абстракциях.

В D, кстати, тоже есть iota:


foreach (i; 100.iota) {}

Да, но 0..100 читается лучше. iota хороша для функциональных цепочек с map, filter, chunk и тд, и ещё для шага отличного от 1, он там третьим аргументом передаётся.
Имхо, это недоработка в языке, что 0..100 это только для foreach. Было бы лучше, если можно было бы в любом месте использовать для обозначения диапазонов. Вместо этого… переиспользовали для срезов и значение там совсем другое.

Самый лучший foreach что я видел — у Kotlin


list.forEach {
   println(it)
}

На scala аналогом будет


list foreach println

А если нам нужно использовать каждый элемент несколько раз?


list.forEach {
   action1(it)
   action2(it)
   ...
}

Пожалуйста:
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.

Чем этот forEach удобнее котлиновского же цикла for, да и цикла for в других языках?


list.forEach {
   println(it)
}

vs


for (it <- list) {
    println(it)
}

Такой метод удобнее, когда цепочка адаптеров итераторов уже весьма длинная.

А почему бы, в Вашем конкретном случае, не запускать вычислания по каждому из направлений параллельно и потом индексиванно сливать их вместе. Понимаю, что немного оффтоп, но по-моему, нужно просто немного оптимизировать алгоритм.
В питоне часть проблем со сложенными или вложенными циклами неплохо решает библиотека itertools (там ещё много других полезных итераторов):
for i, j, k in itertools.product(range(4), range(n), range(3)):
    # do something

Чёрт его знает, как такую штуку реализовать на C/C++, но раз тут мозговой штурм…

Ещё если в проекте много повторяющихся геометрических итераций вида тетраэдры->вершины->грани, то можно заранее создать стандартные списки индексов, по которым потом итерироваться — такой кэшированный результат itertools.product, что снимет проблемы с производительностью.

Когда-то давно этим баловался http://rsdn.org/forum/src/1560894.1
Практика в те годы показала что добиться аналогичной генерации не так легко.
Кроме того такой код менее понятен другим.

Очень редко пишу вложенные циклы. Только если вложенный итератор от внешнего зависит. В остальных случаях, естественно, for I, j, k in itertools.product().

Sign up to leave a comment.

Articles