Всё началось с того, что я в институте и после его окончания писал код на C++ и не знал бед. Но тут в один прекрасный день пришлось писать код под .NET на C#. Сперва немного поплевался, но потом ничего — втянулся. Увидел выгодные отличия от C++: безопасность, строгость и т.д. Также не смог обойти стороной LINQ при работе с коллекциями…
Введение в проблематику
Но всю прелесть LINQ я оценил, когда настала пора вернуться к C++. Было немного непривычно писать на C++, после полугодового перерыва. Ничто не предвещало беды, как вдруг мне нужно было подсчитать сумму элементов в векторе, конкретнее сумму полей элементов вектора. На C# это решалось бы так:
int sum = candles.Sum(c => c.ClosePrice);
Но на C++ выходило:
int sum = 0; for(int i = 0; i < candles.size(); i++) sum += candles[i].ClosePrice;
А если переписать на итераторах:
int sum = 0;
for(auto it = candles.begin(); it != candles.end(); ++it)
sum += it->ClosePrice;
Qt чуть-чуть облегчает ситуацию, но не слишком:
int sum = 0; foreach(Candle candle, candles) sum += candle.ClosePrice;
Также новый стандарт языка C++11 обещает нам упрощение,
но Visual Studio 2010 увы эту фичу не поддерживает:
int sum = 0; for (Candle candle : vector) sum += candle.ClosePrice;
К хорошему быстро привыкаешь. Это был полнейший непорядок. Все эти варианты мне не подходили. Нужно было решение-однострочник. Дальше я стал гуглить и по первой же ссылке нашёл: http://stackoverflow.com/questions/3221812/sum-of-elements-in-a-stdvector
Самое короткое из предложенных решений:
int sum = std::accumulate(vector.begin(), vector.end(), 0);
Но, что делать если складывать надо значения только одного из полей. Можно конечно сделать хитрый итератор, который при разыменовании — возвращает одно из полей… Но всё это попахивает жёстким кодингом для такой простой задачи.
Что же делать?
Следующие 20-30 минут гугления показали что есть Boost Ranges и парочка других библиотек, но они все выглядели не так как выглядит LINQ. В тот самый момент я почувствовал в себе силы — написать свою реализацию и покрыть её тестами.
Основными задачами для меня стали:
- Сделать библиотеку максимально похожую на LINQ
- Сделать весь функционал «отложенным» (ленивым)
Так появился проект boolinq (название сочетает в себе слова bool и linq). Разместил его на Google Code: http://code.google.com/p/boolinq/. И вот что у меня получилось:
int sum = boolinq::from(cnadles).sum([](Candle c){return c.ClosePrice;});
Конечно, выглядит несколько сложнее LINQ. Но, это только за счёт синтаксиса лямбда-выражений в языке C++. Сама структура кода, осталось такой же. На данный момент реализованы следующие функции:
Преобразования последовательностей:
- take(int)
- skip(int)
- concat(range)
- where(lambda)
- select(lambda)
- reverse()
- orderBy()
- orderBy(lambda)
- groupBy(lambda)
- distinct()
- distinct(lambda)
- for_each(lambda)
Аггрегаторы последовательностей:
- all()
- all(lambda)
- any()
- any(lambda)
- sum()
- sum(lambda)
- avg()
- avg(lambda)
- min()
- min(lambda)
- max()
- max(lambda)
- count()
- count(lambda)
- contains(value)
- elementAt(int)
Экспорт последовательности:
- toSet()
- toList()
- toDeque()
- toVector()
- toContainer<T>()
И даже несколько необычных:
- bytes()
- bytes<ByteOrder>()
- unbytes<T>()
- unbytes<T,ByteOrder>()
- bits()
- bits<BitOrder>()
- bits<BitOrder,ByteOrder>()
- unbits()
- unbits<BitOrder>()
- unbits<T>()
- unbits<T,BitOrder>()
- unbits<T,BitOrder,ByteOrder>()
Пример использования
Вот пример выражения:
int src[] = {1,2,3,4,5,6,7,8}; auto dst = from(src).where( [](int a){return a%2 == 1;}) // 1,3,5,7 .select([](int a){return a*2;}) // 2,6,10,14 .where( [](int a){return a>2 && a<12;}) // 6,10 .toVector();
К исходной коллекции пошагово применяются несколько операций:
1. Оставить только элементы с нечётным значением.
2. Умножить значение каждого из элементов на 2.
3. Оставить только элементы со значениями в диапазоне (2,12).
4. Результат поместить в std::vector
.
Или более сложное выражение:
struct Man { std::string name; int age; }; Man src[] = { {"Kevin",14}, {"Anton",18}, {"Agata",17}, {"Terra",20}, {"Layer",15}, }; auto dst = from(src).where( [](const Man & man){return man.age < 18;}) .orderBy([](const Man & man){return man.age;}) .select( [](const Man & man){return man.name;}) .toVector();
Тип у переменной dst
будет std::vector<std::string>
. Результирующий вектор будет содержать следующие значения: «Kevin», «Layer», «Agata». Применяемые к исходному массиву действия:
1. Оставить в массиве только людей моложе 18 лет.
2. Упорядочить элементы в массиве по увеличению возраста.
3. Выбрать из массива только имена.
4. Результат поместить в std::vector
.
Заключение
В результате получилась библиотека отложенного выполнения запросов к массивам, векторам и другим контейнерам с данными. Скорость работы функций не уступает скорости работы аналогичной программы, написанной с использованием циклов. Синтаксис максимально приближен к LINQ. Я с удовольствием провёл время, проектируя и разрабатывая функционал библиотеки. Код неплохо покрыт тестами (не знаю сколько процентов, если кто подскажет — буду рад). Имеются функции, аналогов которых в LINQ — нет.
Распространяется библиотека в виде единого заголовочного-файла boolinq-all.h
. Буду рад, если кому-то библиотека окажется полезна. Если имеются предложения по улучшению, добавлению функции — прошу высказывайтесь. Если есть время и желание покодить — присоединяйтесь. Комментарии к коду на Google Code могут оставлять все желающие. Также создана группа для обсуждения на Google Groups: https://groups.google.com/forum/?fromgroups#!forum/boolinq
Автор: k06a