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


#1

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


(2 курс ФИИТ) CS211. Языки программирования — практика
#2

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. Но в случае с итераторами основания есть, так как их поведение описано в стандарте.

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


#3

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


#4

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

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


#5

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


#7

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


#8

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

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

#9
  1. Нет.
  2. А почему нет? Это же мёртвый код, он всё равно оптимизируется.
  3. Ой, да какая разница, как это написано, работает же?

#10

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


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

#12

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

#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;
}


#13

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

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


#14

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

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


#15
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;

#16

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

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

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

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


#17

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


#18

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

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

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

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


#19

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


#20

Агрессивная оптимизация в коде с 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), и программа уходит в бесконечный цикл.


#21
x * *begin++ + *begin;

или

x * * begin + + + * begin

или

 x ** begin + ++*begin

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