LINQ to Objects на языке C++

в 12:04, , рубрики: .net, c++, linq, Программирование, метки: , ,

Всё началось с того, что я в институте и после его окончания писал код на C++ и не знал бед. Но тут в один прекрасный день пришлось писать код под .NET на C#. Сперва немного поплевался, но потом ничего — втянулся. Увидел выгодные отличия от C++: безопасность, строгость и т.д. Также не смог обойти стороной LINQ при работе с коллекциями…

LINQ to Objects на языке C++

Введение в проблематику

Но всю прелесть 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

Поделиться

* - обязательные к заполнению поля