C для цикла реализован иначе, чем в других языках?

В обзоре книги Кнута «Искусство компьютерного программирования» я прочитал следующее:

«Сама« практичность »означает, что будущий специалист по CS должен изучить ошибки Кернигана при разработке C, в частности тот печально известный факт, что цикл for многократно оценивает условие for, которое дублирует while и не соответствует поведению большинства других языков. которые реализуют цикл for. "

(http://www.amazon.com/review/R9OVJAJQCP78N/ref=cm_cr_pr_viewpnt#R9OVJAJQCP78N)

О чем этот парень говорит? Как можно реализовать цикл for, который не был бы просто синтаксическим сахаром для цикла while?

Просто добавил тег C++, так как ответ на этот вопрос не менее актуален для C++ (типичный шаблон использования: for (iterator i = c.begin (); i! = C.end (); ++ i) // ...)

Magnus Hoff 24.10.2008 13:23
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
13
1
2 556
11
Перейти к ответу Данный вопрос помечен как решенный

Ответы 11

Он, вероятно, имеет в виду циклы for, такие как for i:=0 to N, и циклы for-each, которые перебирают элементы набора. Я подозреваю, что все языки, в которых есть цикл for в стиле C, на самом деле получили его от C.

В точности то, о чем я думал. TAOCP и K&R оба равны Старый. С тех пор от Си унаследовано множество языков.

Bill the Lizard 23.10.2008 17:24

Плюс один. Например, BASIC For петель.

RuntimeException 23.10.2008 18:38
Ответ принят как подходящий

Учти это:

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 () возвращает только константу, то большинство компиляторов оптимизируют это, как вы говорите, но это не всегда так.

Tarski 23.10.2008 17:34

В этом примере я предполагаю, что вы действительно хотите вызвать final_value только один раз. Это громоздко выразить на C. Вполне возможно, что вы хотите, чтобы final_value вызывалась каждый раз, но чаще всего я обнаруживаю, что я хочу, чтобы она вызывалась только один раз.

Magnus Hoff 23.10.2008 17:40

@Tarski: и компиляция не может оптимизировать вызов final_value (), потому что он не может знать, стабильно ли возвращаемое значение. Рассмотрим "for (i = 0; i <strlen (str); ++ i)". Теперь вы можете изменять или не изменять длину str в своем цикле (но в 99,99% случаев вы не будете )

James Curran 23.10.2008 17:44

Компилятор оптимизирует его, если final_value () находится в том же исходном файле, что и цикл.

Jim Buck 23.10.2008 18:19

@Jim: Вы делаете предположения о сложности final_falue () и о том, сколько оптимизатор может / сделает. Некоторые оптимизаторы используют кросс-компиляцию, другие ограничены даже одним исходным файлом.

Darron 23.10.2008 18:53

Извиняюсь. То, что я сказал, было неверным. Бывают случаи, когда компилятор оптимизирует это. Например, если final_value () жестко запрограммирован на возвращение 5 и находится в том же исходном файле, но также и в случаях, когда это невозможно - так чем другие языки отличаются? как эквивалент strlen в PASCAL?

Tarski 23.10.2008 23:16

@Tarski: В Паскале final_value будет вызываться ровно один раз, потому что это семантика цикла for в этом языке. Это даже не считается оптимизацией. :) (Кроме того, эквивалент strlen - это O (1) в Паскале;)

Magnus Hoff 24.10.2008 11:50

@Tarski, что оптимизация может произойти только в том случае, если компилятор действительно может сказать, что функция всегда возвращает одно и то же ... это часто не так.

Spudd86 18.06.2010 00:45

@James, который компилятор, вероятно, оптимизирует, поскольку он, вероятно, знает, что делает strlen.

Spudd86 18.06.2010 00:46

@ spudd86 - это сомнительно, поскольку компилятору также придется отслеживать, что происходит с измеряемой строкой. Кроме того, законно написать свою собственную функцию под названием strlen, которая делает что-то совершенно другое.

James Curran 18.06.2010 15:37

Возможно, петля разворачивается? Если вы знаете, сколько раз будет выполняться цикл 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, что в свою очередь может привести к более медленному коду.

Isak Savo 24.10.2008 12:38

Магнус прав, но следует также отметить, что в большинстве языков (pre-C) условным условием является критерий конец (т.е. «остановить, когда i равно 100»). В C (и большинстве языков post-C) это критерий Продолжать (т. Е. «Продолжать, пока i меньше 100»).

но как вы могли узнать, когда мне было 100, если вы не оценивали его после каждой итерации цикла?

Kip 23.10.2008 18:12

Я просто говорю о синтаксисе, а не об оценке.

James Curran 23.10.2008 18:48

По сути, цикл 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 знает, что в некоторых случаях он может оптимизировать его. (Это «чистая» функция, поскольку она всегда возвращает один и тот же вывод, если ему задан тот же ввод).

Isak Savo 24.10.2008 12:40

Ваша переменная max неправильно названа, лучше называть ее счетчиком, поскольку цикл не будет выполняться с i = max.

Mark Baker 24.10.2008 13:32

Остановка ценить оценивается только один раз. Остановка условие должна оцениваться каждый раз в цикле, чтобы определить, останавливаться или нет. Разница в том, что в C / C++ вы указываете условие остановки напрямую, и это условие может быть любым, каким вы хотите. Однако не существует стоп-значения, которое потенциально можно было бы вычислить только один раз.

David Schwartz 30.04.2012 08:33

@DavidSchwartz Я не понимаю вашего последнего предложения и не согласен с вашим первым ... В C значение остановки вычисляется для каждого цикла. И, конечно же, условие остановки.

PhiLho 02.05.2012 19:28

@PhiLho: Условие цикла можно представить как «<переменная цикла> <оператор> <значение остановки>» (так, в i < 5, i - это переменная цикла, «меньше» - оператор, а 5 - значение остановки). На каждом языке цикл условие оценивается в каждом цикле. В противном случае было бы невозможно выполнить цикл дважды. Вопрос в том, сколько раз оценивается остановка ценить. А в C / C++ нет языковой концепции стоп-значения. В цикле for C / C++ i может быть переменной цикла, а условие может быть foo<bar. Стоп-значение не обязательно, поэтому язык нечего указывать, оценивается один раз.

David Schwartz 02.05.2012 22:37

@DavidSchwartz: условие цикла может иметь любую форму, которая возвращает что-то эквивалентное логическому. Это может быть foo() && bar(), RADIUS > dist(x1, y1, x2, y2), answer == IGNORE или что угодно. На самом деле нет стоп-значение, за исключением результата оценки условия остановки, но ты представил концепцию, поэтому я не вижу смысла в этом обсуждении. А, или, возможно, вы оспариваете мое последнее предложение в моем ответе, который действительно немного двусмыслен ... :-)

PhiLho 03.05.2012 13:06

Я хочу сказать, что язык каждый оценивает условие остановки в каждом цикле. Разница между C и языками, в которых есть что-то, что они оценивают один раз, заключается в том, что эти другие языки имеют стоп-значение, а C - нет.

David Schwartz 03.05.2012 23:00

В x86 цикл for может быть выполнен на уровне сборки без создания цикла while. Команда петля уменьшает значение регистра ecx и переходит к операнду, если ecx больше 0, в противном случае она ничего не делает. Это классический цикл for.

mov ecx, 0x00000010h
loop_start:
;loop body
loop loop_start
;end of loop

Ассемблер здесь не в счет.

DJClayworth 23.10.2008 17:42

Вот почему некоторые скажут вам, если возможно, выполнить цикл в обратном порядке, то есть for (i = limit; i> 0; i--) {any;}. Написание его таким образом может позволить компилятору выполнить эту оптимизацию в двоичном формате.

Kip 23.10.2008 18:15

Если компилятор может доказать, что каждая итерация цикла не зависит от следующей, он может выполнить эту оптимизацию самостоятельно. Я бы сказал, что включение сборки в это обсуждение вполне приемлемо, поскольку C часто называют «кроссплатформенной сборкой».

rmeador 23.10.2008 18:29

Если все, что вам нужно, это простой счетный цикл, тогда

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 вы указываете значение завершения. В обоих случаях условие оценивается каждый раз в цикле.

David Schwartz 30.04.2012 08:31

@DavidSchwartz - есть разница между проверкой выполнения цикла и повторной проверкой значения этой итерации для q при подготовке к проверке выполнения цикла. Замените q расчетом или вызовом функции, и, возможно, вы заметите проблему. Также обратите внимание, что на самом деле будет вычисление нет на каждой итерации, если цикл развернут, что компилятор Ada выше мог бы тривиально сделать, но компилятор C может быть не в состоянии, в зависимости от того, что находится в «Сделайте что-нибудь».

T.E.D. 02.05.2012 17:54

Возможно, Кнут имеет в виду БЕЙСИК.

В 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 для.

Таким образом, очевидно, что неудача одного стиля - это успех другого стиля. На этом субъективная часть обсуждения закончится.

Возможно, for Кернигана следовало называть циклом?

Просто шучу? Возможно нет.

Автор обзора, по-видимому, решил, что каждая языковая конструкция должна вести себя одинаково в разных языках. Так что переименование для в петля облегчило бы его / ее дискомфорт.

Проблема в том, что автор не понимает, что 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.

fuz 12.12.2014 15:48

Я собирался проголосовать за тебя вплоть до того момента, когда ты процитировал дьявола ...

n00bmind 05.03.2021 12:18

Другие вопросы по теме