Новая битва (++i) vs (i++)

Жду экспериментов, опровергающих/подтверждающих тезис из знаменитого поста на старом форуме!

3 лайка

Stackoverflow-меня-окончательно-убедил-в моей неправоте.

На всякий случай проверил g++. Без флагов оптимизации этот код

#include <iostream>
#include <vector>

using namespace std;

int main()
{
    for (int i = 0; i < 10; i++)
        cout << i << " ";
    cout << endl;
    return 0;
}

компилируется в тот-же, что и код с преинкрементом. Но такой код

#include <iostream>
#include <vector>

using namespace std;

int main()
{
    vector<int> vec = {1, 2, 5, -2, 3};
    for (auto it = begin(vec); it != end(vec); it++)
        cout << *it << " ";
    cout << endl;
    return 0;
}

без как минимум -O1 уже не оптимизируется. Для собственных переопределённых инкрементов оптимизация вообще не всегда возможна, так как там могут происходить совершенно противоположные действия. То есть у компилятора нет оснований на равносильную замену x++ на ++x. Но в случае с итераторами основания есть, так как их поведение описано в стандарте.

Думаю вопрос можно считать закрытым.

Какой же, однако, интересный пост! Много я пропустил :frowning:

А давайте устроим крохотный эксперимент!

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

Не могу понять задачу. Надо “сказать, что вы о нём думаете” или “дописать документацию” ?

Что вы думаете, когда сели писать документацию =)

Итак, мои мысли: 0) Есть ли у нас тесты на эту функцию?

  1. Зачем нужна 13 и 15 строчка
  2. Почему мы пишем на си, но используем компилятор cи++? (От c++ здесь только cout, зато размер массива мы передаём как дополнительный аргумент, я плакал)
1 лайк
  1. Нет.
  2. А почему нет? Это же мёртвый код, он всё равно оптимизируется.
  3. Ой, да какая разница, как это написано, работает же?

Да, кстати, реквестирую красивые решения этой задачи!

  1. Плохо.
  2. Не знаю как вам, а мне мёртвый код мешает при чтении программ.
  3. Если работает/не работает это единственный критерий оценки, тогда, действительно, никакой разницы нет. Но я подумал, что это хоть и главный, но всё же не единственный критерий. Лучше ответьте: почему там ассемблерную вставку не сделали? Тоже работать будет.

Мой вариант(конечно, можно лучше):

#include <iostream>
#include <cassert>
#include <iterator>

using namespace std;

template <typename A, typename RandomAccessIterator>
auto gorner(A x, RandomAccessIterator begin, RandomAccessIterator end) -> decltype(x * *begin)
{
    auto result = x * *begin++ + *begin;
    while (begin != end)
    {
        result *= x;
        result += *++begin;
    }
    return result;
} 

int main()
{
   cout << "Gorner" << endl; 
   
   double fs1[] { 4.5, 0, -4 };
   cout << gorner(1, begin(fs1), end(fs1)) << endl;
   cout << gorner(-2, begin(fs1), end(fs1)) << endl;
   
   return 0;
}

О, в точку! Оптимизируемость/неоптимизируемость — не единственный, и обычно далеко не самый важный критерий при написании кода. Мёртвый код оптимизируется, конечно, но не имеет смысла и даже мешает.

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

1 лайк

Ну, итераторы это хорошо, конечно, но RandomAccessIterator тут не нужен.

А можете переписать цикл while в for без тела?

for (auto result = x * *begin++ + *begin; 
     begin != end; 
     result *= x, result += *++begin) { }

Действительно, формально не требуется, но всё-же коэффициенты многочлена это упорядоченный набор, а ForwardIterator используется в неупорядоченных контейнерах. Следующий пример будет работать, но зачем?

   set<double> fs1 { 4.5, 0, -4 };
   cout << gorner(1, begin(fs1), end(fs1)) << endl;
   cout << gorner(-2, begin(fs1), end(fs1)) << endl;

Можно, только тут одна лишняя итерация, как и в исходном коде с i <= n (должно быть строго).

Я бы так написала:

auto result = *begin++;
for (; begin != end; result = result*x + *begin++);
return result;

И это как раз будет хорошим примером, когда действительно удобно использовать постинкремент.

Ну, а линейный список, например? Произвольный доступ это слишком сильное требование, здесь вполне достаточно InputIterator. Я понимаю вашу мысль, но всю семантику мы пока выразить не можем, поэтому из двух зол, как известно…

Здесь будет неопределённое поведение, так как неизвестно, какое выражение вычислится раньше, begin++ или правый *begin. Почитайте про точки следования, это аналогично x = x++;.

Циклов с путым телом и раздутой третьей частью заголовка следует избегать: это очень частый источник ошибок (хороший недавний пример). Лучше:

auto result = *beg++;
for (; beg != end; ++beg)
    result = result * x + *beg;
return result;

В качестве бонуса вы избавляетесь от постинкремента в цикле, который, как известно, грозит временными объектами.

Это полный бред. Например, итератор списка это Forward. Не говоря уже об итераторах потоков (получаем многочлен по сети), которые Input (Output).

Агрессивная оптимизация в коде с UB вообще говоря страшные вещи творить может. Например (уже исправлено, но суть отражает):

void foo(int * array)
{
    int val = 0x3020100;
    for (int i = 0; i < 64; i++)
    {
        array[i] = val;
        val += 0x4040404;
    }

Суть: компилятор выбрасывает i (для массива заводит указатель и двигает его, а проверку выхода осуществляет при помощи сравнения val с каким-то числом). На 63 итерации произойдет overflow переменной val (UB), и программа уходит в бесконечный цикл.

x * *begin++ + *begin;

или

x * * begin + + + * begin

или

 x ** begin + ++*begin

Боже, как же я люблю C++.