Relinx — ще одна реалізація .NET LINQ методів на C++, з підтримкою «ледачих обчислень»

RelinxLogoСеред багатьох реалізацій LINQ-подібних бібліотек C++ є багато цікавих, корисних і ефективних. Але на мій погляд, більшість з них написані з певною зневагою до C++ як до мови. Весь код цих бібліотек написаний так, ніби намагаються виправити його «потворність». Зізнаюся, я люблю C++. І як би її не поливали брудом, моя любов до нього навряд чи пройде. Можливо, це частково тому, що це моя перша мова програмування високого рівня і другий, який я вивчив після Асемблера.



— Навіщо?
Це одвічне і, цілком природне запитання. «Навіщо, коли є море LINQ-подібних бібліотек — бери і користуйся?». Частково, я написав її через свого власного бачення реалізації таких бібліотек. Частково через бажання користуватися бібліотекою, яка максимально повно реалізує LINQ методи, щоб при необхідності можна було б переносити код з мінімальними змінами з однієї мови в інший.

Особливості моєї реалізації:

  • Використання стандарту C++14 (зокрема, поліморфні лямбда виразу)
  • Використання ітераторів-адаптерів тільки з послідовним доступом (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_eachfor_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_manyselect_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 є проблеми компіляції юніт тестів. Наскільки мені вдалося з'ясувати, цей компілятор неправильно розуміє деякі складні лямбда виразу. Наприклад, я помітив, що якщо використовувати метод 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,Пухнастий"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/

Готовий відповісти на питання по використанню бібліотеки і буду дуже вдячний за конструктивні пропозиції щодо покращення функціоналу і помічені помилки.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.