Подтвердить что ты не робот

Есть ли основание для этого альтернативного синтаксиса цикла for?

Я встретил набор слайдов для обсуждения rant на С++. Там были интересные интересные лакомые кусочки, но мне показался слайд 8. Его содержимое было приблизительно:

Сменяющиеся стили

Старый и разоренный:

for (int i = 0; i < n; i++)

Новая жара:

for (int i(0); i != n; ++i)

Я никогда не видел цикл for, используя вторую форму раньше, поэтому требование о том, что это была "Новая жара", заинтересовала меня. Я могу понять некоторые причины:

  • Прямая инициализация с использованием конструктора против инициализации копирования
  • != может быть быстрее в оборудовании, чем <
  • ++i не требует, чтобы компилятор сохранял старое значение i, что и было бы i++.

Я бы предположил, что это преждевременная оптимизация, поскольку современные оптимизирующие компиляторы будут скомпилировать эти два до тех же самых инструкций; во всяком случае, последнее хуже, потому что это не "нормальный" цикл for. Использование != вместо < также подозрительно для меня, потому что это делает цикл семантически отличным от исходной версии и может привести к некоторым редким, но интересным ошибкам.

Была ли какая-нибудь точка, в которой популярна была версия "Новая горячность" цикла for? Есть ли причина использовать эту версию в наши дни (2016+), например. необычные типы переменных цикла?

4b9b3361

Ответ 1

  • Прямая инициализация с использованием конструктора против инициализации копирования

    Они идентичны для int и будут генерировать идентичный код. Используйте то, что вы предпочитаете читать, или какие ваши политики кода и т.д.

  • != может быть быстрее в оборудовании, чем <

    Сгенерированный код фактически не будет i < n vs i != n, он будет как i - n < 0 vs i - n == 0. То есть вы получите jle в первом случае и je во втором случае. Все команды jcc имеют одинаковую производительность (см. ссылка на набор инструкций и ссылка на опции, в которой перечислены все команды jcc вместе с пропускной способностью 0,5).

    Что лучше? Для int s, вероятно, не имеет значения с точки зрения производительности.

    Строго безопаснее делать <, если вы хотите пропустить элементы в середине, так как вам не придется беспокоиться о том, чтобы закончиться бесконечным циклом /w 90 > . Но просто напишите условие, которое имеет смысл писать с помощью цикла, который вы пишете. Также посмотрите ответ dasblinkenlight.

  • ++i не требует, чтобы компилятор сохранял старое значение i, что и будет i++.

    Да, это вздор. Если ваш компилятор не может сказать, что вам не нужно старое значение и просто перепишите i++ в ++i, тогда получите новый компилятор. Они определенно скомпилируются с тем же кодом с одинаковой производительностью.

    Тем не менее, это хорошая рекомендация просто использовать правильную вещь. Вы хотите увеличить i, чтобы ++i. Используйте только пост-прирост, когда вам нужно использовать пост-прирост. Полная остановка.


Тем не менее, настоящая "новая жара" определенно будет:

for (int i : range(n)) { ... }

Ответ 2

Вы правы в оптимизации операторов компиляторов и префикса и постфикса ++. Это не имеет значения для int, но это важно, когда вы используете итераторы.

Вторая часть вашего вопроса более интересна:

Использование != вместо < также подозрительно для меня, потому что это делает цикл семантически отличным от исходной версии и может привести к некоторым редким, но интересным ошибкам.

Я бы перефразировал его как "можно уловить некоторые редкие, но интересные, ошибки".

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

a != b слабее, чем a < b, потому что a < b подразумевает, что a != b, но a != b не означает, что a < b. Поэтому a != b обеспечивает лучшее условие продолжения.

В очень простых выражениях вы знаете, что a == b сразу же после завершения цикла с a != b; с другой стороны, когда цикл с a < b завершен, все, что вы знаете, это a >= b, что не так хорошо, как знание точного равенства.

Ответ 3

Мне лично не нравится второй, я бы использовал:

for (int i = 0; i < n; ++i); //A combination of the two :)

int i = 0 vs int i(0)

Никакой разницы вообще, они даже не скомпилируются с одними и теми же инструкциями по сборке (без оптимизации):

int main()
{
    int i = 0; //int i(0);
}

int i = 0 версия:

main:
    pushq   %rbp
    movq    %rsp, %rbp
    movl    $0, -4(%rbp)
    movl    $0, %eax
    popq    %rbp
    ret

int i(0) версия:

main:
    pushq   %rbp
    movq    %rsp, %rbp
    movl    $0, -4(%rbp)
    movl    $0, %eax
    popq    %rbp
    ret

i < n vs i != n

Вы правы, что != может ввести некоторые интересные ошибки:

for (int i = 0; i != 3; ++i)
{
    if (i == 2)
        i += 2; //Oops, infinite loop
    //...
}

Сравнение != в основном используется для итераторов, которые не определяют оператор < (или >). Возможно, это то, что имел в виду автор?

Но здесь вторая версия явно лучше, поскольку она более четко заявляет о намерении, чем другая (и вводит меньше ошибок).


i++ vs ++i

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

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

int main()
{
    int i = 0;
    i++; //++i;
}

i++ версия:

main:
    pushq   %rbp
    movq    %rsp, %rbp
    movl    $0, -4(%rbp)
    addl    $1, -4(%rbp)
    movl    $0, %eax
    popq    %rbp
    ret

++i версия:

main:
    pushq   %rbp
    movq    %rsp, %rbp
    movl    $0, -4(%rbp)
    addl    $1, -4(%rbp)
    movl    $0, %eax
    popq    %rbp
    ret

Ответ 4

Эти две формы никоим образом не связаны с производительностью. Важно то, как вы пишете код вообще. Следуйте аналогичным шаблонам и сосредоточьтесь на выразительности и лаконичности. Поэтому для инициализации предпочитаем int я (0) (или лучше: я {0}), поскольку это подчеркивает, что это инициализация, а не назначение. Для сравнения разница между = = < заключается в том, что вы устанавливаете более низкие требования к своему типу. Для целых чисел разницы нет, но в целом итераторы могут поддерживать меньше, чем, но всегда должны поддерживать равенство. Наконец, приращение приставки выражает ваше намерение лучше, потому что вы не используете результат.

Ответ 5

В этом коде это не имеет значения. Но я предполагаю, что автор стремится использовать тот же стиль стиля стиля для всех циклов for (за исключением основанных на диапазоне, предположительно).

Например, если у нас был какой-то другой класс как тип счетчика циклов

for ( Bla b = 0; b < n; b++ )

то возникают проблемы:

  • Bla b = 0; может не скомпилироваться, если Bla не имеет доступного конструктора copy/move
  • b < n может не скомпилироваться, если Bla не допускает определения слабого порядка или определяет operator<.
  • b++ может не скомпилироваться или иметь непреднамеренные побочные эффекты, так как пост-приращение обычно возвращает значение

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


Обратите внимание, что это обсуждение может продолжаться вечно. Мы могли бы сказать, что int i{}; лучше, потому что некоторые типы могут не допускать 0 в качестве инициализатора. Тогда мы могли бы сказать, что, если n не было int? Это действительно должно быть decltype(n) i{}. Или на самом деле мы должны использовать цикл на основе диапазона, который устраняет все вышеперечисленные проблемы. И так далее.

Итак, в конце дня это все равно личное предпочтение.

Ответ 6

i!= n

Честно говоря, слайд 8 потерял меня прямо там, и вы правы с подозрением, что что-то может быть не совсем правильно.

Помимо высокой вероятности того, что современный компилятор будет иметь такие виды циклов, оптимизированных так же тщательно, как это теоретически возможно для текущих процессоров, поощряя программиста писать менее надежный код, чтобы "помочь оптимизатору" по любой причине просто oldfashioned и не имеет места вообще в современном мире, ИМО. Реальная стоимость программного обеспечения - это человеческое время в эти дни, а не процессорное время, за 99,99...% всех проектов.

На метауровне истины не имеют места в руководящих принципах кодирования. Слайд, устанавливающий соглашение о кодировании без предоставления объективных и продуманных причин для этого, бесполезен. Заметьте, что я был бы в порядке с такой причиной, как "мы делаем это так, потому что мы хотим выбрать один стиль вместо множества", мне не нужна техническая причина - только по какой-то причине. Затем я могу решить, принимаю ли я аргументы за руководство по кодированию или нет (и, таким образом, принимаю руководство).

Ответ 7

Стилистически лучше использовать постфиксное приращение/декремент в цикле for C-стиля, когда это возможно,

for (i = 0; i < n; i++)

а не

for (i = 0; i < n; ++i)

С помощью постфиксного оператора в левой части всех трех выражений появляется переменная цикла i, что упрощает чтение кода. Кроме того, если вам нужно выполнить любое значение, отличное от 1, вы должны записать его с помощью i слева...

for (i = 0; i < n; i += 2)

поэтому для согласованности это также должно быть сделано таким образом, когда вы используете сокращенную ++ для += 1. (Как указано в комментариях, нам было бы легче читать += 1, чем ++, если бы у нас не было 45-летнего обучения, преподающего нам ++.)

При использовании таких фундаментальных типов, как int, приращение префикса/постфикса не будет иметь разницы в производительности; как показали другие ответы, компилятор будет генерировать точно такой же код в любом случае. С итераторами оптимизатор должен сделать больше работы, чтобы исправить это, но я не думаю, что есть какие-либо оправдания для компилятора С++ 11-эпохи, который не справляется с постфиксным оператором ++ так же эффективно, как префикс operator ++, когда они используются исключительно для их побочные эффекты.

(Конечно, если вы застряли с итератором, который поддерживает только префикс ++, тогда вы пишете его таким образом.)