Все мы, C++ программисты, несомненно любим STL. Действительно, без неё многие вещи приходилось бы писать своими руками. Но иногда STL вызывает боль и страдания. Недавно я столкнулся с тем, что типичное для стандартных алгоритмов решение, принимать два итератора first и last, оказалось неудобным в моём простом проекте.
Просьба меня не судить, так как всё что вы увидите ниже – всего лишь попытка борьбы со сложностью в своём проекте и было сделано под сильным стремлением к субъективной красоте кода.
Например, стоит задача пройтись по контейнеру с элементами. При этом поведение программы, естественно, будет меняться от элемента к элементу, в зависимости от их значений. Легко? Казалось бы, чего сложного, но давайте рассмотрим эту ситуацию с обычным вектором из строк. Вот наша функция, выполняющая эту задачу:
Но как мы помним, при определённом значении текущего элемента, поведение должно изменяться. Таких значений, а значит и вариантов различного поведения будет много, поэтому не будем плодить полотна if-else в цикле обхода, а создадим отдельную функцию. Что может быть проще чем:
А теперь нам нужно реализовать ещё один вариант поведения, для ещё одного значения текущей позиции. Думаю, можно не продолжать, и так ясно к чему это приводит. Мы всегда вынуждены передавать текущую и последнюю позиции. Создание общего для функций класса проблемы не решит, так как текущее значение всё равно нужно будет сравнивать с последним, а значит придется выносить std::vector<std::string>::const_iterator &last как отдельную константу.
И тут я вспомнил как удобно сделаны итераторы-перечислители в LINQ и Java, ведь для нашего случая не требуется арифметика указателей, использование операторов += и -= для std::vector<std::string>::const_iterator curr. Всё что нам нужно – возможность удобно обойти контейнер изменяя поведение в зависимости от значений. Дальше вы увидите мой велосипед для LINQ-подобных перечислителей в C++.
Создадим .h файл для нашего эксперимента и определим в нём пространство имен Dq, это и будет названием для мини-библиотечки.
Уже лучше, теперь в нужную функцию можно передавать один аргумент-перечислитель. В C++17 такой перечислитель создать и использовать очень просто:
Ну а пока стандарт окончательно не утвердили, напишем вспомогательную функцию:
И будем использовать её так:
Вооружившись перечислителем, вот как можно переписать наш первый пример:
По мне это решение намного красивее и удобнее для обыденных задач чем возня с STL итераторами. Теперь о маленьком недочёте, такой итератор не получится использовать для стандартных алгоритмов. Но это к счастью легко поправимо, просто добавим в класс Enumerator методы.
и
Весь код целиком можно увидеть одним файлом на GitHub.
Подведем итоги:
Теперь у нас есть удобная обертка-перечислитель для стандартных итераторов совместимая с алгоритмами STL. Жизнь стала чуточку веселее, всемирной энтропии из-за затрат на написание статьи прибавилось, а об объективной полезности моего велосипеда судить вам).
Просьба меня не судить, так как всё что вы увидите ниже – всего лишь попытка борьбы со сложностью в своём проекте и было сделано под сильным стремлением к субъективной красоте кода.
Например, стоит задача пройтись по контейнеру с элементами. При этом поведение программы, естественно, будет меняться от элемента к элементу, в зависимости от их значений. Легко? Казалось бы, чего сложного, но давайте рассмотрим эту ситуацию с обычным вектором из строк. Вот наша функция, выполняющая эту задачу:
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. Жизнь стала чуточку веселее, всемирной энтропии из-за затрат на написание статьи прибавилось, а об объективной полезности моего велосипеда судить вам).