В чем разница между преинкрементом (++i) и постинкрементом (i++) в C++?

Ответ

Разница заключается в значении, которое возвращает оператор, и в потенциальной эффективности.

Семантика

  • Преинкремент (++i): "Увеличить, затем использовать".
    1. Значение i увеличивается на 1.
    2. Возвращается ссылка на уже изменённый объект i.
  • Постинкремент (i++): "Использовать, затем увеличить".
    1. Сохраняется копия исходного значения i.
    2. Значение i увеличивается на 1.
    3. Возвращается сохранённая копия (старое значение).

Наглядный пример с встроенными типами:

int i = 5;
int a = ++i; // Шаг 1: i = 6. Шаг 2: a = i (т.е. 6).
// Результат: i == 6, a == 6

int j = 5;
int b = j++; // Шаг 1: сохранить старое j (5). Шаг 2: j = 6. Шаг 3: b = старое значение (5).
// Результат: j == 6, b == 5

Вопрос эффективности (для пользовательских типов)

Для встроенных типов (int, char) современные компиляторы генерируют идентичный оптимизированный код, и разницы в скорости нет.

Однако для типов с перегруженными операторами (например, итераторы) ++i обычно предпочтительнее:

class MyIterator {
    // Преинкремент: работает с текущим объектом, возвращает ссылку.
    MyIterator& operator++() {
        // ... логика инкремента ...
        return *this;
    }
    // Постинкремент: должен создать копию ДО изменения, вернуть её.
    MyIterator operator++(int) { // int — фиктивный параметр для отличия сигнатур
        MyIterator temp = *this; // ВОТ ОНА, ДОПОЛНИТЕЛЬНАЯ КОПИЯ!
        ++(*this);               // Используем уже реализованный преинкремент
        return temp;             // Возвращаем старую копию
    }
};

Создание временного объекта в operator++(int) может быть накладным для сложных типов.

Рекомендация (best practice):

  • В циклах for всегда используйте ++i. Это привычка, которая гарантирует максимальную эффективность для любых типов.
    for (auto it = vec.begin(); it != vec.end(); ++it) { // Хорошо
    // for (...; ...; it++) { // Менее оптимально для не-POD типов
  • Используйте i++ только тогда, когда вам действительно нужно старое значение в выражении.
    auto old_value = counter++; // Логично: сохраняем старое значение, счётчик растёт.
    process(ptr++);             // Обработать текущий элемент, затем сдвинуть указатель.

Ответ 18+ 🔞

А, ну это классика, блядь! Сейчас объясню, в чём тут собака зарыта, а то народ путается, как последний распиздяй.

Смотри, суть в том, что именно возвращает эта штука и насколько она тебе мозги выносит по производительности.

Что они творят, эти уроды?

  • Преинкремент (++i): Логика проще пареной репы. "Сперва накинуть, потом отдать".
    1. Берёшь i и нахуяриваешь ему единичку сверху.
    2. Возвращаешь ссылку на этого уже обдолбанного i. Всё честно.
  • Постинкремент (i++): А вот это уже хитрая жопа. "Сперва отдать, потом накинуть".
    1. Ты делаешь копию того, чем i был до того, как всё пошло по пизде.
    2. Потом уже самому i прибавляешь единицу.
    3. А на выход подсовываешь ту самую сохранённую копию, старую, не тронутую. Подстава, да?

Смотри на пальцах с обычными числами:

int i = 5;
int a = ++i; // Шаг 1: i стало 6. Шаг 2: a получает i (то есть 6).
// Итог: i == 6, a == 6. Всё прозрачно, как слёзы ребёнка.

int j = 5;
int b = j++; // Шаг 1: запомнили старый j (5). Шаг 2: j стало 6. Шаг 3: b получает то самое старое значение (5).
// Итог: j == 6, b == 5. Вот тебе и "использовать, потом увеличить". Сам от себя охуел?

А где тут собака порылась? (Про эффективность)

С обычными типами вроде int — да похуй, честно. Компилятор сейчас такой умный, что разницы нихуя не будет, оптимизирует на раз.

Но вот когда дело доходит до твоих собственных классов, или там итераторов — тут начинается ёперный театр. ++i почти всегда лучше, и вот почему:

class MyIterator {
    // Преинкремент: работает с собой, возвращает себя же.
    MyIterator& operator++() {
        // ... делаем свои грязные делишки ...
        return *this; // И всё, доволен как слон.
    }
    // Постинкремент: должен создать копию ДО того, как начнёт ебашить.
    MyIterator operator++(int) { // Этот int — просто финт ушами, чтобы компилятор их различил
        MyIterator temp = *this; // ВОТ ОНА, БЛЯДЬ, ДОПОЛНИТЕЛЬНАЯ КОПИЯ! Лишняя работа!
        ++(*this);               // Используем уже готовый преинкремент, чтобы не повторяться
        return temp;             // А возвращаем-то старьё!
    }
};

Создание этого временного объекта temp в operator++(int) — это лишняя нагрузка, особенно если объект тяжёлый. Зачем плодить сущности, а?

Так как же жить, спросишь ты?

  • В циклах for — всегда пиши ++i. Выработай привычку, и будет тебе счастье. Это гарантирует, что даже с итераторами от какого-нибудь ёбанько-контейнера не будет лишних телодвижений.
    for (auto it = vec.begin(); it != vec.end(); ++it) { // Отлично! Красава!
    // for (...; ...; it++) { // Так-то тоже работает, но для сложных штук может быть медленнее. Не надо так.
  • А i++ оставь для тех редких случаев, когда тебе реально нужно старое значение прямо в выражении. Тогда да, без него никуда.
    auto old_value = counter++; // Логично: сохранили, что было, а счётчик уже ушёл вперёд.
    process(ptr++);             // Обработали текущий элемент, а указатель сами того не заметив, уже сдвинули. Удобно!

Вот и вся магия. Главное — понимать, что возвращаешь, и не создавать лишних копий на ровном месте. А то доверия к такому коду — ноль ебать.