Что такое bidirectional iterator в C++ STL?

Ответ

Bidirectional iterator (двунаправленный итератор) — это одна из категорий итераторов в C++, которая расширяет возможности forward iterator, добавляя возможность перемещаться не только вперед, но и назад по элементам последовательности.

Ключевые операции, которые он поддерживает (помимо операций forward iterator):

  • Префиксный и постфиксный декремент: --it и it-- (переход к предыдущему элементу).
  • Все операции forward iterator: инкремент (++), разыменование (*), сравнение (==, !=).

Какие контейнеры STL предоставляют двунаправленные итераторы:

  • std::list
  • std::set, std::multiset
  • std::map, std::multimap
  • std::deque (также предоставляет random access iterator)

Пример использования:

#include <list>
#include <iostream>
#include <algorithm>

int main() {
    std::list<int> numbers = {10, 20, 30, 40, 50};

    // 1. Движение вперед и назад
    auto it = numbers.begin();
    std::advance(it, 2); // it указывает на 30
    std::cout << *it << std::endl; // 30

    --it; // Движение назад к 20
    std::cout << *it << std::endl; // 20

    // 2. Обратный итератор (адаптер, построенный на основе двунаправленного)
    for (auto rit = numbers.rbegin(); rit != numbers.rend(); ++rit) {
        std::cout << *rit << ' '; // Выведет: 50 40 30 20 10
    }
    std::cout << std::endl;

    // 3. Алгоритмы, требующие двунаправленных итераторов
    // std::reverse требует двунаправленных итераторов
    std::reverse(numbers.begin(), numbers.end());
    // numbers теперь {50, 40, 30, 20, 10}

    // 4. Многопроходность гарантирована
    auto it1 = numbers.begin();
    auto it2 = numbers.begin(); // Два независимых итератора
    ++it1;
    // it2 по-прежнему указывает на начало, можно снова пройти по контейнеру.

    return 0;
}

Ограничения bidirectional iterator (по сравнению с random access iterator):

  • Нет арифметики с целыми числами: Нельзя it + 5 или it += 3. Для перемещения на N позиций нужно использовать std::advance(it, N) или цикл.
  • Нет операций сравнения <, >, <=, >=. Только == и !=.
  • Нет оператора индексации [].

Практический вывод: Если ваш алгоритм или функция должны работать с контейнерами вроде std::list или std::map, они должны быть написаны с учетом ограничений двунаправленных итераторов, используя только операции ++, --, *, == и !=. Это обеспечивает максимальную универсальность.

Ответ 18+ 🔞

А, ну вот, очередная порция итераторов подъехала. Слушай, это как с машинами: есть просто вперёд, а есть с задней передачей. Так вот bidirectional iterator — это как раз та самая задняя передача, ёпта.

Двунаправленный итератор — это такой тип, который умеет всё то же, что и обычный однонаправленный (forward), но ещё может и пятиться назад, как краб обиженный. Не только ++, но и --, представляешь?

Что он умеет, кроме базового набора:

  • Идти назад: --it и it--. То есть если накосячил и проскочил нужный элемент, можно вернуться, а не ходить по кругу с начала, как дурак.
  • Всё остальное от предка: шагать вперёд, смотреть на элемент (*) и сравнивать.

Где такое чудо встечается в STL:

  • std::list — классика, связный список.
  • std::set, std::multiset — деревья.
  • std::map, std::multimap — тоже деревья, но с ключами.
  • std::deque — он вообще крутой, у него и рандомный доступ есть, но и двунаправленный он тоже даёт, хитрая жопа.

Смотри, как этим пользоваться:

#include <list>
#include <iostream>
#include <algorithm>

int main() {
    std::list<int> numbers = {10, 20, 30, 40, 50};

    // 1. Туда-сюда, как в лифте
    auto it = numbers.begin();
    std::advance(it, 2); // упёрлись в 30
    std::cout << *it << std::endl; // 30

    --it; // Откатились на 20
    std::cout << *it << std::endl; // 20

    // 2. Обратный проход (это вообще отдельная магия на основе двунаправленности)
    for (auto rit = numbers.rbegin(); rit != numbers.rend(); ++rit) {
        std::cout << *rit << ' '; // Выведет: 50 40 30 20 10
    }
    std::cout << std::endl;

    // 3. Алгоритмы, которые без возможности ходить назад — просто сдохнут
    // std::reverse — его бы без --it не было бы вообще
    std::reverse(numbers.begin(), numbers.end());
    // Теперь numbers = {50, 40, 30, 20, 10}

    // 4. Многопроходность — это святое. Запустил два итератора — и пусть ползают.
    auto it1 = numbers.begin();
    auto it2 = numbers.begin(); // Второй, независимый
    ++it1;
    // а it2 всё ещё в начале, можно заново всё пройти.

    return 0;
}

А теперь, блядь, ложка дёгтя. Чем он НЕ умеет (в отличие от random access iterator):

  • Арифметики нет, ёпта! Нельзя написать it + 5. Вообще. Придётся или std::advance(it, N) вызывать, или в цикле прыгать, что для списка — пиздец как долго.
  • Сравнивать на больше/меньше нельзя. Только == и !=. Не спросишь, который итератор «левее».
  • Квадратных скобочек [] тебе тоже не видать. Это не массив, чувак.

Суть в чём: если пишешь универсальный алгоритм, который должен работать и со списками, и с мапами — забудь про it + n и прочие радости жизни. Работай только через ++, --, * и сравнение. Тогда твой код будет летать везде, а не только в векторах да массивах. Иначе будет тебе хиросима, когда попробуешь этот алгоритм к std::list прикрутить.