В обзоре книги Кнута «Искусство компьютерного программирования» я прочитал следующее:
«Сама« практичность »означает, что будущий специалист по CS должен изучить ошибки Кернигана при разработке C, в частности тот печально известный факт, что цикл for многократно оценивает условие for, которое дублирует while и не соответствует поведению большинства других языков. которые реализуют цикл for. "
(http://www.amazon.com/review/R9OVJAJQCP78N/ref=cm_cr_pr_viewpnt#R9OVJAJQCP78N)
О чем этот парень говорит? Как можно реализовать цикл for, который не был бы просто синтаксическим сахаром для цикла while?





Он, вероятно, имеет в виду циклы for, такие как for i:=0 to N, и циклы for-each, которые перебирают элементы набора. Я подозреваю, что все языки, в которых есть цикл for в стиле C, на самом деле получили его от C.
В точности то, о чем я думал. TAOCP и K&R оба равны Старый. С тех пор от Си унаследовано множество языков.
Плюс один. Например, BASIC For петель.
Учти это:
for i:=0 to 100 do { ... }
В этом случае мы могли бы заменить окончательное значение 100 вызовом функции:
for i:=0 to final_value() do { ... }
... и функция final_value вызывается только один раз.
Однако в C:
for (int i=0; i<final_value(); ++i) // ...
... функция final_value будет вызываться для каждой итерации цикла, что делает хорошей практикой быть более многословной:
int end = final_value();
for (int i=0; i<end; ++i) // ...
Я не согласен. Как узнать, что значение final_value () не будет каждый раз возвращать новое значение? Если final_value () возвращает только константу, то большинство компиляторов оптимизируют это, как вы говорите, но это не всегда так.
В этом примере я предполагаю, что вы действительно хотите вызвать final_value только один раз. Это громоздко выразить на C. Вполне возможно, что вы хотите, чтобы final_value вызывалась каждый раз, но чаще всего я обнаруживаю, что я хочу, чтобы она вызывалась только один раз.
@Tarski: и компиляция не может оптимизировать вызов final_value (), потому что он не может знать, стабильно ли возвращаемое значение. Рассмотрим "for (i = 0; i <strlen (str); ++ i)". Теперь вы можете изменять или не изменять длину str в своем цикле (но в 99,99% случаев вы не будете )
Компилятор оптимизирует его, если final_value () находится в том же исходном файле, что и цикл.
@Jim: Вы делаете предположения о сложности final_falue () и о том, сколько оптимизатор может / сделает. Некоторые оптимизаторы используют кросс-компиляцию, другие ограничены даже одним исходным файлом.
Извиняюсь. То, что я сказал, было неверным. Бывают случаи, когда компилятор оптимизирует это. Например, если final_value () жестко запрограммирован на возвращение 5 и находится в том же исходном файле, но также и в случаях, когда это невозможно - так чем другие языки отличаются? как эквивалент strlen в PASCAL?
@Tarski: В Паскале final_value будет вызываться ровно один раз, потому что это семантика цикла for в этом языке. Это даже не считается оптимизацией. :) (Кроме того, эквивалент strlen - это O (1) в Паскале;)
@Tarski, что оптимизация может произойти только в том случае, если компилятор действительно может сказать, что функция всегда возвращает одно и то же ... это часто не так.
@James, который компилятор, вероятно, оптимизирует, поскольку он, вероятно, знает, что делает strlen.
@ spudd86 - это сомнительно, поскольку компилятору также придется отслеживать, что происходит с измеряемой строкой. Кроме того, законно написать свою собственную функцию под названием strlen, которая делает что-то совершенно другое.
Возможно, петля разворачивается? Если вы знаете, сколько раз будет выполняться цикл for, вы можете буквально скопировать и вставить содержимое цикла. Большинство циклов while будут основаны на каком-то условии, которое не является простым подсчетом от 0 до N, поэтому эту оптимизацию нельзя будет использовать.
Возьмите этот пример
int x;
for (x = 10; x != 0; --x)
{
printf ("Hello\n");
}
Я знаю, что вы обычно делаете x = 0; x <= 10; ++x, но все это будет раскрыто в сборке.
какая-то псевдосборка:
mov 10, eax
loop:
print "hello"
dec eax
jne loop
В этом примере мы продолжаем возвращаться назад по циклу, чтобы 10 раз печатать «привет». Однако мы оцениваем условие с помощью инструкции jne каждый раз в цикле.
Если бы мы развернули его, мы могли бы просто положить
print "hello"
print "hello"
print "hello"
print "hello"
print "hello"
print "hello"
print "hello"
print "hello"
print "hello"
print "hello"
Никакие другие инструкции нам не понадобятся, так что это быстрее. Это всего лишь простой пример - я постараюсь найти лучший!
Вы имеете ввиду, что мог будет быстрее? Большой размер кода может привести к пропускам кеш-памяти L1 / L2, что в свою очередь может привести к более медленному коду.
Магнус прав, но следует также отметить, что в большинстве языков (pre-C) условным условием является критерий конец (т.е. «остановить, когда i равно 100»). В C (и большинстве языков post-C) это критерий Продолжать (т. Е. «Продолжать, пока i меньше 100»).
но как вы могли узнать, когда мне было 100, если вы не оценивали его после каждой итерации цикла?
Я просто говорю о синтаксисе, а не об оценке.
По сути, цикл for C (а также Java, JavaScript и множество языков, производных от C) действительно является синтаксическим сахаром для цикла while:
for (int i = 0; i < max; i++) { DoStuff(); }
которая является очень актуальной идиомой, строго эквивалентна:
int i = 0; while (i < max) { DoStuff(); i++; }
(Не говоря уже о проблемах области видимости, которые в любом случае менялись в разных версиях C.)
Условие остановки оценивается на каждой итерации. В некоторых случаях это может быть интересно, но может быть ловушкой: я видел i < strlen(longConstantString) в источнике, что является основным способом замедлить работу программы, потому что strlen - дорогостоящая функция в C.
.
Каким-то образом цикл for в основном предназначен для запуска несколько раз, о чем известно заранее, вы все равно можете использовать break для раннего завершения, поэтому динамическая оценка условия остановки больше раздражает, чем полезна: если вам действительно нужна динамическая оценка, вы используете while () {} (или do {} while ()).
На некоторых других языках, таких как Lua, условие остановки оценивается только один раз, при инициализации цикла. Это помогает прогнозировать поведение цикла и часто более производительно.
strlen является особенным, поскольку, по крайней мере, GCC знает, что в некоторых случаях он может оптимизировать его. (Это «чистая» функция, поскольку она всегда возвращает один и тот же вывод, если ему задан тот же ввод).
Ваша переменная max неправильно названа, лучше называть ее счетчиком, поскольку цикл не будет выполняться с i = max.
Остановка ценить оценивается только один раз. Остановка условие должна оцениваться каждый раз в цикле, чтобы определить, останавливаться или нет. Разница в том, что в C / C++ вы указываете условие остановки напрямую, и это условие может быть любым, каким вы хотите. Однако не существует стоп-значения, которое потенциально можно было бы вычислить только один раз.
@DavidSchwartz Я не понимаю вашего последнего предложения и не согласен с вашим первым ... В C значение остановки вычисляется для каждого цикла. И, конечно же, условие остановки.
@PhiLho: Условие цикла можно представить как «<переменная цикла> <оператор> <значение остановки>» (так, в i < 5, i - это переменная цикла, «меньше» - оператор, а 5 - значение остановки). На каждом языке цикл условие оценивается в каждом цикле. В противном случае было бы невозможно выполнить цикл дважды. Вопрос в том, сколько раз оценивается остановка ценить. А в C / C++ нет языковой концепции стоп-значения. В цикле for C / C++ i может быть переменной цикла, а условие может быть foo<bar. Стоп-значение не обязательно, поэтому язык нечего указывать, оценивается один раз.
@DavidSchwartz: условие цикла может иметь любую форму, которая возвращает что-то эквивалентное логическому. Это может быть foo() && bar(), RADIUS > dist(x1, y1, x2, y2), answer == IGNORE или что угодно. На самом деле нет стоп-значение, за исключением результата оценки условия остановки, но ты представил концепцию, поэтому я не вижу смысла в этом обсуждении. А, или, возможно, вы оспариваете мое последнее предложение в моем ответе, который действительно немного двусмыслен ... :-)
Я хочу сказать, что язык каждый оценивает условие остановки в каждом цикле. Разница между C и языками, в которых есть что-то, что они оценивают один раз, заключается в том, что эти другие языки имеют стоп-значение, а C - нет.
В x86 цикл for может быть выполнен на уровне сборки без создания цикла while. Команда петля уменьшает значение регистра ecx и переходит к операнду, если ecx больше 0, в противном случае она ничего не делает. Это классический цикл for.
mov ecx, 0x00000010h
loop_start:
;loop body
loop loop_start
;end of loop
Ассемблер здесь не в счет.
Вот почему некоторые скажут вам, если возможно, выполнить цикл в обратном порядке, то есть for (i = limit; i> 0; i--) {any;}. Написание его таким образом может позволить компилятору выполнить эту оптимизацию в двоичном формате.
Если компилятор может доказать, что каждая итерация цикла не зависит от следующей, он может выполнить эту оптимизацию самостоятельно. Я бы сказал, что включение сборки в это обсуждение вполне приемлемо, поскольку C часто называют «кроссплатформенной сборкой».
Если все, что вам нужно, это простой счетный цикл, тогда
for (i=0; i<100; i++) dostuff();
будет хорошо, и компилятор может его оптимизировать.
Если вы используете функцию в части continue оператора for, например
for (i=0; i<strlen(s); i++) dostuff();
тогда функция будет оцениваться каждый раз, и это обычно не очень хорошая идея, поскольку накладные расходы функции замедлят ваш процесс. Иногда это может замедлить ваш процесс до невозможности использования.
Если возвращаемое значение функции не изменится во время итерации, извлеките его из цикла:
slen = strlen(s);
for (i=0; i<slen; i++) dostuff();
Но бывают случаи, когда функция будет возвращать разные значения при каждом вызове, и тогда вы не хотите, чтобы она извлекалась из цикла:
for (isread(fd, &buffer, ISFIRST);
isstat(fd) >= 0;
isread(fd, &buffer, ISNEXT)
{
dostuff(buffer);
}
и вы хотите, чтобы он оценивался каждый раз. (Это слегка надуманный пример, основанный на моей работе, но он показывает потенциал).
C дает вам простую возможность свернуть вашу петлю любым способом. Вы должны знать, как должен работать ваш цикл, и оптимизировать его, насколько это возможно, в зависимости от ваших потребностей.
Последний пример можно было бы выразить в виде цикла while:
isread(fd, &buffer, ISFIRST);
while (isstat(fd) >= 0)
{
dostuff(buffer);
isread(fd, &buffer, ISNEXT);
}
но это не так аккуратно, и если я использую continue в цикле, мне придется снова вызвать итерацию isread. Помещение всего этого в цикл for делает его более аккуратным и гарантирует, что итерация isread вызывается каждым циклом.
Я пишу функции нижнего уровня, чтобы их можно было использовать в таких циклах for. Он объединяет все элементы цикла while, чтобы вам было легче понять его.
В Аде (и я считаю, что в большинстве других языков, производных от Алгола) условие завершения цикла for оценивается только один раз, в начале цикла. Например, предположим, что у вас есть следующий код на Аде
q := 10;
for i in 1..q loop
q := 20;
--// Do some stuff
end loop;
Этот цикл будет повторяться ровно 10 раз, потому что q было 10 при запуске цикла. Однако, если я напишу, казалось бы, эквивалентный цикл на C:
q = 10;
for (int i=0;i<q;i++) {
q = 20;
// Do some stuff
}
Затем цикл повторяется 20 раз, потому что q было изменено на 20 к тому времени, когда я стал достаточно большим, чтобы это имело значение.
Конечно, способ C более гибкий. Однако это имеет ряд негативных последствий. Очевидно, что программе приходится тратить усилия на повторную проверку состояния цикла каждый цикл. Хороший оптимизатор может быть достаточно умен, чтобы обойти проблему в простых случаях, подобных этому, но что произойдет, если «q» является глобальным и «сделать что-нибудь» включает вызов процедуры (и, таким образом, теоретически может изменить q)?
Сложный факт в том, что мы просто знаем путь о цикле Ada больше, чем о цикле C. Это означает, что при том же уровне интеллекта и усилий в оптимизаторе Ada может намного лучше выполнять работу по оптимизации. Например, компилятор Ada знает, что он может заменить весь цикл 10 копиями содержимого, независимо от того, что это за содержимое. Оптимизатор C должен будет изучить и проанализировать содержимое.
На самом деле это лишь один из многих способов, при которых дизайн синтаксиса C поджимает компилятор.
Фактически, условие должно быть оценено каждый time. Если бы условие было оценено только один раз, оно было бы либо истинным, либо ложным, и цикл либо зацикливался бы вечно, либо только один раз. Разница между C и Ada заключается в том, что в C вы указываете условие завершения, в то время как в Ada вы указываете значение завершения. В обоих случаях условие оценивается каждый раз в цикле.
@DavidSchwartz - есть разница между проверкой выполнения цикла и повторной проверкой значения этой итерации для q при подготовке к проверке выполнения цикла. Замените q расчетом или вызовом функции, и, возможно, вы заметите проблему. Также обратите внимание, что на самом деле будет вычисление нет на каждой итерации, если цикл развернут, что компилятор Ada выше мог бы тривиально сделать, но компилятор C может быть не в состоянии, в зависимости от того, что находится в «Сделайте что-нибудь».
Возможно, Кнут имеет в виду БЕЙСИК.
В BASIC (старом, про VB не знаю) циклы FOR были реализованы по-другому.
Например. перебрать десять раз
ДЛЯ N = от 1 до 10
...
СЛЕДУЮЩИЙ N
---- ИЛИ ЖЕ ---- найти сумму четных чисел меньше 100
ДЛЯ N = 0 ДО 100 ШАГ 2
...
СЛЕДУЮЩИЙ N
В C у вас может быть любое условие в "for" для проверки выхода из цикла. Думаю, более гибкая.
Чего эти пуристы, кажется, никогда не осознают, так это всей суть C, и, в некоторой степени, C++ дает возможность реализовать то, что вы хотите и как вы этого хотите. По общему признанию, если результат вашего конечного выражения изменится, было бы лучше просто использовать цикл while. Возможно, проблема в том, что у программистов создается впечатление, что C реализует "высокий уровень", но каждый должен знать, что C - это довольно низкоуровневый язык.
The very 'practicality' means that the would-be CS major has to learn Kernighan's mistakes in designing C, notably the infamous fact that a for loop evaluates the for condition repeatedly, which duplicates while and fails to match the behavior of most other languages which implement a for loop.
Муаахахаха!
Мне нравятся основные (каламбурные) утверждения рецензента:
Для меня это пахнет кем-то, кому так и не удалось овладеть основными функциями / философией C.
Поскольку я все еще изучал физику в университете, я обнаружил, что C (то есть C-подобные языки) должны стать моим предпочтительным языком, когда я обнаружил цикл C для.
Таким образом, очевидно, что неудача одного стиля - это успех другого стиля. На этом субъективная часть обсуждения закончится.
Просто шучу? Возможно нет.
Автор обзора, по-видимому, решил, что каждая языковая конструкция должна вести себя одинаково в разных языках. Так что переименование для в петля облегчило бы его / ее дискомфорт.
Проблема в том, что автор не понимает, что C для является расширением пока, а не другим циклом. Это означает, что пока является синтаксической упрощенной версией для, вместо пока других языков, где для, возможно, был уменьшен.
Цитируя автора обзора, он делает следующие утверждения о для и пока:
Наличие ортогональных элементов - это хорошо, когда их можно комбинировать. Но на всех языках, которые я знаю, невозможно объединить для и пока вместе.
Пример: что, если для языка рецензента вам нужен цикл, идущий от начала до конца (например, для), но все еще способный оценивать на каждой итерации цикла (например, пока): вам может потребоваться найти в подмножестве контейнера (а не всего контейнера), значение согласно некоторому тесту с отслеживанием состояния?
В C-подобном языке вы используете для. На идеальном языке рецензента вы просто взламываете какой-нибудь уродливый код, плача, потому что у вас нет цикла в течении некоторого времени (или цикла C для).
Я считаю, что мы можем резюмировать критику C (и C-подобных языков) со стороны рецензента следующим утверждением «Печально известная ошибка Кернигана в том, что он не предоставил C синтаксис предыдущих существующих языков», которое можно резюмировать снова как «Никогда не думай иначе».
Цитата Бьярна Страуструпа:
There are only two kinds of languages: the ones people complain about and the ones nobody uses.
Так что в целом комментарий рецензента следует рассматривать как похвалу.
^ _ ^
Между прочим, в языке программирования Go есть только цикл for с особым случаем, когда только один оператор предоставляется в заголовке цикла, и в этом случае он ведет себя как while в C.
Я собирался проголосовать за тебя вплоть до того момента, когда ты процитировал дьявола ...
Просто добавил тег C++, так как ответ на этот вопрос не менее актуален для C++ (типичный шаблон использования: for (iterator i = c.begin (); i! = C.end (); ++ i) // ...)