Среди многих реализаций LINQ-подобных библиотек на C++, есть много интересных, полезных и эффективных. Но на мой взгляд, большинство из них написаны с неким пренебрежением к C++ как к языку. Весь код этих библиотек написан так, словно пытаются исправить его «уродливость». Признаюсь, я люблю C++. И как бы его не поливали грязью, моя любовь к нему едва ли пройдёт. Возможно, это отчасти потому, что это мой первый язык программирования высокого уровня и второй, который я изучил после Ассемблера.
Зачем?
Это извечный и, вполне, естественный вопрос. «Зачем, когда есть море LINQ-подобных библиотек — бери и пользуйся?». Отчасти, я написал её из-за своего собственного видения реализации таких библиотек. Отчасти, из-за желания пользоваться библиотекой, которая максимально полно реализует LINQ методы, чтобы при необходимости можно было бы переносить код с минимальными изменениями из одного языка в другой.
Особенности моей реализации:
- Использование стандарта C++14 (в частности, полиморфные лямбда выражения)
- Использование итераторов-адаптеров только c последовательным доступом (forward-only/input iterators). Это позволяет использовать любые типы контейнеров и объектов, которые не могут иметь произвольного доступа по разным причинам, например std::forward_list. Это, также, немного упрощает разработку пользовательских объектов-коллекций, которые должны поддерживать std::begin, std::end, а сами итераторы должны поддерживать только operator *, operator != и operator ++. Таким образом, кстати, работает новый оператор for для пользовательских типов.
- Relinx объект подходит для итерации в новом операторе for без конвертации в другой тип контейнера, а также в других STL функциях-алгоритмах в зависимости от типа итератора нативного контейнера.
- Библиотека реализует почти все варианты LINQ методов в том или ином виде.
- Relinx объект является очень тонкой прослойкой над нативной коллекцией, насколько это возможно.
- В библиотеке используется форвардинг параметров и реализуется move семантика вместо copy, где это уместно.
- Библиотека достаточно быстрая, за исключением операций, которые требуют произвольный доступ к элементам коллекции (например, last, element_at, reverse).
- Библиотека легко расширяемая.
- Библиотека распространяется под лицензией MIT.
Некоторые программисты C++ не любят итераторы и пытаются их как-то заменить, например на ranges, или обойтись вообще без них. Но, в новом стандарте C++11, чтобы поддерживать оператор for для пользовательских объектов-коллекций, необходимо предоставить для оператора for именно итераторы (или итерируемые типы, например, указатели). И это требование не просто STL, а уже самого языка.
Таблица соответствия LINQ методов Relinx методам:
LINQ методы | Relinx методы |
---|---|
Aggregate | aggregate |
All | all |
none | |
Any | any |
AsEnumerable | from |
Avarage | avarage |
Cast | cast |
Concat | concat |
Contains | contains |
Count | count |
cycle | |
DefaultIfEmpty | default_if_empty |
Distinct | distinct |
ElementAt | element_at |
ElementAtOrDefault | element_at_or_default |
Empty | from |
Except | except |
First | first |
FirstOrDefault | first_or_default |
for_each, for_each_i | |
GroupBy | group_by |
GroupJoin | group_join |
Intersect | intersect_with |
Join | join |
Last | last |
LastOrDefault | last_or_default |
LongCount | count |
Max | max |
Min | min |
OfType | of_type |
OrderBy | order_by |
OrderByDescending | order_by_descending |
Range | range |
Repeat | repeat |
Reverse | reverse |
Select | select, select_i |
SelectMany | select_many, select_many_i |
SequenceEqual | sequence_equal |
Single | single |
SingleOrDefault | single_or_default |
Skip | skip |
SkipWhile | skip_while, skip_while_i |
Sum | sum |
Take | take |
TakeWhile | take_while, take_while_i |
ThenBy | then_by |
ThenByDescending | then_by_descending |
ToArray | to_container, to_vector |
ToDictionary | to_map |
ToList | to_list |
ToLookup | to_multimap |
to_string | |
Union | union_with |
Where | where, where_i |
Zip | zip |
Как?
Исходный код библиотеки документирован Doxygen блоками с примерами использования методов. Также, имеются простые юнит-тесты, в основном написанные мною для контроля и соответствия результатов исполнения методов результатам C#. Но, они сами могут служить простыми примерами использования библиотеки. Для написания и тестирования я использовал компиляторы MinGW / GCC 5.3.0, Clang 3.9.0 и MSVC++ 2015. C MSVC++ 2015 есть проблемы компиляции юнит тестов. Насколько мне удалось выяснить, этот компилятор неправильно понимает некоторые сложные lambda выражения. Например, я заметил, что если использовать метод from внутри лямбды, то вылетает странная ошибка компиляции. С другими перечисленными компиляторами таких проблем нет.
Библиотека представляет из себя только заголовочный файл, который необходимо включить в модуль, где она будет использована.
Перед использованием, для удобства, можно ещё заинджектить relinx namespace.
Несколько примеров использования:
Простое использование. Просто, посчитаем количество нечётных чисел:
auto result = from({1, 2, 3, 4, 5, 6, 7, 8, 9}).count([](auto &&v) { return !!(v % 2); });
std::cout << result << std::endl;
//Должно быть выведено: 5
Пример по-сложнее — группировка:
struct Customer
{
uint32_t Id;
std::string FirstName;
std::string LastName;
uint32_t Age;
bool operator== (const Customer &other) const
{
return Id == other.Id && FirstName == other.FirstName && LastName == other.LastName && Age == other.Age;
}
};
//auto group_by(KeyFunction &&keyFunction) const noexcept -> decltype(auto)
std::vector<Customer> t1_data =
{
Customer{0, "John"s, "Doe"s, 25},
Customer{1, "Sam"s, "Doe"s, 35},
Customer{2, "John"s, "Doe"s, 25},
Customer{3, "Alex"s, "Poo"s, 23},
Customer{4, "Sam"s, "Doe"s, 45},
Customer{5, "Anna"s, "Poo"s, 23}
};
auto t1_res = from(t1_data).group_by([](auto &&i) { return i.LastName; });
auto t2_res = from(t1_data).group_by([](auto &&i) { return std::hash<std::string>()(i.LastName) ^ (std::hash<std::string>()(i.FirstName) << 1); });
assert(t1_res.count() == 2);
assert(t1_res.first([](auto &&i){ return i.first == "Doe"s; }).second.size() == 4);
assert(t1_res.first([](auto &&i){ return i.first == "Poo"s; }).second.size() == 2);
assert(from(t1_res.first([](auto &&i){ return i.first == "Doe"s; }).second).contains([](auto &&i) { return i.FirstName == "Sam"s; }));
assert(from(t1_res.first([](auto &&i){ return i.first == "Poo"s; }).second).contains([](auto &&i) { return i.FirstName == "Anna"s; }));
assert(t2_res.single([](auto &&i){ return i.first == (std::hash<std::string>()("Doe"s) ^ (std::hash<std::string>()("John"s) << 1)); }).second.size() == 2);
assert(t2_res.single([](auto &&i){ return i.first == (std::hash<std::string>()("Doe"s) ^ (std::hash<std::string>()("Sam"s) << 1)); }).second.size() == 2);
Результатом группировки является последовательность из std::pair, где first является ключом, а second — это сгруппированные по этому ключу элементы Customer в контейнере std::vector. Группировка по нескольким полям одного класса производиться по хэш-ключу в данном примере, но это не обязательно.
А вот, пример использования group_join, который, кстати, не компилируется только в MSVC++ 2015 из-за вложенного relinx запроса в самих lambda выражениях:
struct Customer
{
uint32_t Id;
std::string FirstName;
std::string LastName;
uint32_t Age;
bool operator== (const Customer &other) const
{
return Id == other.Id && FirstName == other.FirstName && LastName == other.LastName && Age == other.Age;
}
};
struct Pet
{
uint32_t OwnerId;
std::string NickName;
bool operator== (const Pet &other) const
{
return OwnerId == other.OwnerId && NickName == other.NickName;
}
};
//auto group_join(Container &&container, ThisKeyFunction &&thisKeyFunction, OtherKeyFunction &&otherKeyFunction, ResultFunction &&resultFunction, bool leftJoin = false) const noexcept -> decltype(auto)
std::vector<Customer> t1_data =
{
Customer{0, "John"s, "Doe"s, 25},
Customer{1, "Sam"s, "Doe"s, 35},
Customer{2, "John"s, "Doe"s, 25},
Customer{3, "Alex"s, "Poo"s, 23},
Customer{4, "Sam"s, "Doe"s, 45},
Customer{5, "Anna"s, "Poo"s, 23}
};
std::vector<Pet> t2_data =
{
Pet{0, "Spotty"s},
Pet{3, "Bubble"s},
Pet{0, "Kitty"s},
Pet{3, "Bob"s},
Pet{1, "Sparky"s},
Pet{3, "Fluffy"s}
};
auto t1_res = from(t1_data).group_join(t2_data,
[](auto &&i) { return i.Id; },
[](auto &&i) { return i.OwnerId; },
[](auto &&key, auto &&values)
{
return std::make_pair(key.FirstName + " "s + key.LastName,
from(values).
select([](auto &&i){ return i.NickName; }).
order_by().
to_string(","));
}
).order_by([](auto &&p) { return p.first; }).to_vector();
assert(t1_res.size() == 3);
assert(t1_res[0].first == "Alex Poo"s && t1_res[0].second == "Bob,Bubble,Fluffy"s);
assert(t1_res[1].first == "John Doe"s && t1_res[1].second == "Kitty,Spotty"s);
assert(t1_res[2].first == "Sam Doe"s && t1_res[2].second == "Sparky"s);
auto t2_res = from(t1_data).group_join(t2_data,
[](auto &&i) { return i.Id; },
[](auto &&i) { return i.OwnerId; },
[](auto &&key, auto &&values)
{
return std::make_pair(key.FirstName + " "s + key.LastName,
from(values).
select([](auto &&i){ return i.NickName; }).
order_by().
to_string(","));
}
, true).order_by([](auto &&p) { return p.first; }).to_vector();
assert(t2_res.size() == 6);
assert(t2_res[1].second == std::string() && t2_res[3].second == std::string() && t2_res[5].second == std::string());
В примере, результатом первой операции является объединение двух различных объектов по ключу методом inner join, а затем их группировка по ним.
Во второй операции, происходит объединение по ключу методом left join. Об этом говорит последний параметр метода установленный в true.
А вот, пример использования фильтрации полиморфных типов:
//auto of_type() const noexcept -> decltype(auto)
struct base { virtual ~base(){} };
struct derived : public base { virtual ~derived(){} };
struct derived2 : public base { virtual ~derived2(){} };
std::list<base*> t1_data = {new derived(), new derived2(), new derived(), new derived(), new derived2()};
auto t1_res = from(t1_data).of_type<derived2*>();
assert(t1_res.all([](auto &&i){ return typeid(i) == typeid(derived2*); }));
assert(t1_res.count() == 2);
for(auto &&i : t1_data){ delete i; };
Я разместил код на нескольких площадках:
GitHub: https://github.com/Ptomaine/Relinx
CodePlex: https://relinx.codeplex.com/
Sourceforge: https://sourceforge.net/projects/relinx/
Готов ответить на вопросы по использованию библиотеки и буду очень благодарен за конструктивные предложения по улучшению функционала и замеченные ошибки.
Автор: arlen_albert