Pull to refresh

Relinx — ещё одна реализация .NET LINQ методов на C++, с поддержкой «ленивых вычислений»

Reading time 7 min
Views 7.8K
RelinxLogo
(ОБНОВЛЕНО!)
Среди многих реализаций LINQ-подобных библиотек на C++, есть много интересных, полезных и эффективных. Но на мой взгляд, большинство из них написаны с неким пренебрежением к C++ как к языку. Весь код этих библиотек написан так, словно пытаются исправить его «уродливость». Признаюсь, я люблю C++. И как бы его не поливали грязью, моя любовь к нему едва ли пройдёт. Возможно, это отчасти потому, что это мой первый язык программирования высокого уровня и второй, который я изучил после Ассемблера.



Важное обновление: Большое изменение в Relinx! relinx_object теперь является миксином от std::enable_shared_from_this и используется как std::shared_ptr. Это изменение позволяет размещать relinx_object в heap-памяти и управлять циклом жизни всей цепочки трансформаций. Теперь std::shared_ptr<relinx_object> можно передавать в функции и потоки без его материализации в контейнер. Единственное изменение в коде пользователя — это замена доступа к объекту через ->, а не через точку, например: раньше from({1, 2, 3}).count(), теперь from({1, 2, 3})->count(). И последнее, код Relinx перекачевал в мой другой проект, который называется nstd, который можно найти здесь.

Зачем?


Это извечный и, вполне, естественный вопрос. «Зачем, когда есть море 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/nstd, https://github.com/Ptomaine/Relinx

Готов ответить на вопросы по использованию библиотеки и буду очень благодарен за конструктивные предложения по улучшению функционала и замеченные ошибки.
Tags:
Hubs:
+12
Comments 25
Comments Comments 25

Articles