Жду экспериментов, опровергающих/подтверждающих тезис из знаменитого поста на старом форуме!
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. Но в случае с итераторами основания есть, так как их поведение описано в стандарте.
Думаю вопрос можно считать закрытым.
Какой же, однако, интересный пост! Много я пропустил
А давайте устроим крохотный эксперимент!
Я прошу студентов посмотреть на вот этот код и сказать, что вы о нём думаете. Вот прям любые мысли в рамках законодательства и правил форума. Представьте, что он вам достался в наследство от предыдущего разработчика, а вас попросили дописать документацию.
Не могу понять задачу. Надо “сказать, что вы о нём думаете” или “дописать документацию” ?
Что вы думаете, когда сели писать документацию =)
Итак, мои мысли: 0) Есть ли у нас тесты на эту функцию?
- Зачем нужна 13 и 15 строчка
- Почему мы пишем на си, но используем компилятор cи++? (От c++ здесь только cout, зато размер массива мы передаём как дополнительный аргумент, я плакал)
- Нет.
- А почему нет? Это же мёртвый код, он всё равно оптимизируется.
- Ой, да какая разница, как это написано, работает же?
Да, кстати, реквестирую красивые решения этой задачи!
- Плохо.
- Не знаю как вам, а мне мёртвый код мешает при чтении программ.
- Если работает/не работает это единственный критерий оценки, тогда, действительно, никакой разницы нет. Но я подумал, что это хоть и главный, но всё же не единственный критерий. Лучше ответьте: почему там ассемблерную вставку не сделали? Тоже работать будет.
Мой вариант(конечно, можно лучше):
#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++
.
Ну, итераторы это хорошо, конечно, но 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++.