Все мы, C++ программисты, несомненно любим STL. Действительно, без неё многие вещи приходилось бы писать своими руками. Но иногда STL вызывает боль и страдания. Недавно я столкнулся с тем, что типичное для стандартных алгоритмов решение, принимать два итератора first и last, оказалось неудобным в моём простом проекте.

Просьба меня не судить, так как всё что вы увидите ниже – всего лишь попытка борьбы со сложностью в своём проекте и было сделано под сильным стремлением к субъективной красоте кода.

Например, стоит задача пройтись по контейнеру с элементами. При этом поведение программы, естественно, будет меняться от элемента к элементу, в зависимости от их значений. Легко? Казалось бы, чего сложного, но давайте рассмотрим эту ситуацию с обычным вектором из строк. Вот наша функция, выполняющая эту задачу:

void Action(std::vector<std::string>::const_iterator curr, const std::vector<std::string>::const_iterator &last) {
    if (curr == last) {
        return;
    }
    // ...
}

Но как мы помним, при определённом значении текущего элемента, поведение должно изменяться. Таких значений, а значит и вариантов различного поведения будет много, поэтому не будем плодить полотна if-else в цикле обхода, а создадим отдельную функцию. Что может быть проще чем:

void ActionTwo(std::vector<std::string>::const_iterator curr, const std::vector<std::string>::const_iterator &last) {
    if (curr == last) {
        return;
    }
    // ...
}
void Action(std::vector<std::string>::const_iterator curr, const std::vector<std::string>::const_iterator &last) {
    if (curr == last) {
        return;
    }
    // ...
    if (*curr == ANY_VALUE) {
        ActionTwo(curr + 1, last);
    }
    // ...
}

А теперь нам нужно реализовать ещё один вариант поведения, для ещё одного значения текущей позиции. Думаю, можно не продолжать, и так ясно к чему это приводит. Мы всегда вынуждены передавать текущую и последнюю позиции. Создание общего для функций класса проблемы не решит, так как текущее значение всё равно нужно будет сравнивать с последним, а значит придется выносить std::vector<std::string>::const_iterator &last как отдельную константу.

И тут я вспомнил как удобно сделаны итераторы-перечислители в LINQ и Java, ведь для нашего случая не требуется арифметика указателей, использование операторов += и -= для std::vector<std::string>::const_iterator curr. Всё что нам нужно – возможность удобно обойти контейнер изменяя поведение в зависимости от значений. Дальше вы увидите мой велосипед для LINQ-подобных перечислителей в C++.

Создадим .h файл для нашего эксперимента и определим в нём пространство имен Dq, это и будет названием для мини-библиотечки.


#pragma once

namespace Dq
{
    // Два шаблонных параметра: Container и ..Args отвечают за контейнер STL и его шаблонные параметры соответственно.
    template <template <typename...> typename Container, typename ...Args>
    // Наш итератор-перечислитель
    class Enumerator {
    private:
        // Сыылка на контейнер
        Container<Args...> &List = Container<Args...>();
        // Текущая позиция на одну меньше стартовой
        typename Container<Args...>::iterator Position = List.begin() - 1;

    public:
        
        // Функция перемещает итератор на один шаг и возвращает результат проверки новой позиции
        bool MoveNext() {
            return ++Position != List.end();
        }

        // Функция возвращает значение текущей позиции
        typename Container<Args...>::value_type &operator*() const {
            return *Position;
        }
        
        // Функция сбрасывает текущую позицию обхода к начальной
        void Reset() {
            Position = List.begin() - 1;
        }
    
        // Конструктор инициализирует ссылку на контейнер
        explicit Enumerator(const Container<Args...> &cont) : List(cont) {}
    };
}

Уже лучше, теперь в нужную функцию можно передавать один аргумент-перечислитель. В C++17 такой перечислитель создать и использовать очень просто:


std::vector values { 0, 1, 2, 3, 4, 5 };
Dq::Enumerator i(values);

Ну а пока стандарт окончательно не утвердили, напишем вспомогательную функцию:


namespace Dq {
    // Просто шаблонная обёртка для нашего конструктора
    template <template <typename...> typename Container, typename ...Args>
    Enumerator<Container, Args...> GetEnumerator(const Container<Args...> &cont) {
        return Enumerator<Container, Args...>(cont);
    }
}

И будем использовать её так:


std::vector<int> values{ 0, 1, 2, 3, 4, 5 };
auto i = Dq::GetEnumerator(values);

Вооружившись перечислителем, вот как можно переписать наш первый пример:


void ActionTwo(auto &position) {
    if (!position.MoveNext()) {
        return;
    }

    // ...
}
void Action(auto &position) {
    if (!position.MoveNext()) {
        return;
    }

    // ...

    if (*position == ANY_VALUE) {
        ActionTwo();
    }

    // ...
}

По мне это решение намного красивее и удобнее для обыденных задач чем возня с STL итераторами. Теперь о маленьком недочёте, такой итератор не получится использовать для стандартных алгоритмов. Но это к счастью легко поправимо, просто добавим в класс Enumerator методы.


template <template <typename...> typename Container, typename ...Args>
    typename Container<Args...>::iterator 
Enumerator<Container, Args...>::CurrentPostion() const {
    return Position;
}

и


template <template <typename...> typename Container, typename ...Args>
    typename Container<Args...>::iterator 
Enumerator<Container, Args...>::LastPostion() const {
    return List.end();
}

Весь код целиком можно увидеть одним файлом на GitHub.

Подведем итоги:

Теперь у нас есть удобная обертка-перечислитель для стандартных итераторов совместимая с алгоритмами STL. Жизнь стала чуточку веселее, всемирной энтропии из-за затрат на написание статьи прибавилось, а об объективной полезности моего велосипеда судить вам).
Поделиться с друзьями
-->

Комментарии (9)


  1. saluev
    24.03.2017 17:47
    +5

    Чем конечный вариант (очень странного) примера лучше изначального варианта?


    1. Daniro_San
      24.03.2017 17:55
      -2

      Тем что теперь стало меньше кода, выглядит (лично для меня) более читаемым и понятным.
      Один аргумент вместо двух, шаг вперёд теперь совмещён с проверкой на конец контейнера.
      По поводу странности: Я ориентировался на то как сделаны перечислители в Linq.


      1. saluev
        24.03.2017 18:02
        +11

        Во-первых, кода столько же.
        Во-вторых, повышенная читабельность под сомнением: все плюсовики привыкли к STL-стилю и легче прочитают изначальный вариант.
        В-третьих, уменьшение числа аргументов не только понизило сложность, но и уменьшило универсальность: теперь алгоритм нельзя применить к части контейнера. С тем же успехом вы могли просто поставить единственным аргументом ссылку на вектор.
        Насчёт странности: я имел в виду не ваше решение, а поставленную изначально задачу. Вы не могли бы привести какой-нибудь более-менее жизненный пример задачи, решение которой вы упростили таким образом? Возможно, станет легче понять, что вы имели в виду, и увидеть преимущества вашего решения.


  1. Fil
    24.03.2017 17:57
    +6

    А почему вас не устраивает так:


    std::vector<int> values { 0, 1, 2, 3, 4, 5 };
    for (auto i : values)
        std::cout << i;


  1. encyclopedist
    24.03.2017 19:08
    +2

    Зачем


    template <typename...> typename Container

    если можно просто typename Container. Вам ведь по отдельности аргументы не нужны, а появляется искусственное ограничение что контейнер может быть только шаблоном.


    В ещё можно просто воспользоваться range-v3.


  1. ivlis
    24.03.2017 22:02
    +1

    По-моему, проще сделать конечный автомат, который будет хранить в state обработчик, который нужно применять сейчас. Тогда у вас будут обработчики, которые ничего не знают про перечисление и перечислитель, который вызывает нужную функцию.


  1. Zifix
    25.03.2017 12:44
    -7

    Все мы, C++ программисты, несомненно любим STL.
    Те, кто использует Qt — уже не C++ программисты?


    1. DarkEld3r
      27.03.2017 11:21
      +1

      Можно использоват Qt и любить STL. Не так уж они и пересекаются, как кажется. Контейнеры и алгоритмы в Qt всё равно имеют STL-совместимый интерфейс (хотя, вроде, это можно и "отключить"), более того, последние так уже вполне официально рекомендуют использовать из STL.


  1. McAaron
    31.03.2017 03:01

    Наверное я не понял постановку задачи — простой цикл for() по контейнеру почему не подходит?

    #include <iostream>
    #include <vector>
    #include <string>
    
    void do_anything(std::string &);
    
    int main () {
    
        std::vector<std::string> strings = {"first", "second", "third", "fourth"};
    
        for (auto it : strings) {
            do_anything(it);
        }
    }
    
    void do_anything(std::string &str) {
        std::cout << str << std::endl;
    }