Relinx - another implementation of .NET LINQ methods in C ++, with support for "lazy computing"

Relinxlogo
(UPDATED!)
Among the many implementations of LINQ-like libraries in C ++, there are many interesting, useful and effective. But in my opinion, most of them are written with some disregard for C ++ as a language. All the code in these libraries is written as if trying to fix its ugliness. I admit, I love C ++. And no matter how it is poured mud, my love for him is unlikely to pass. Perhaps this is partly because it is my first high-level programming language and the second that I learned after Assembler.



Important update : Big change in Relinx! relinx_object is now a mixin from std :: enable_shared_from_this and is used as std :: shared_ptr. This change allows you to place relinx_object in heap-memory and control the life cycle of the entire transformation chain. Now std :: shared_ptrcan be transferred to functions and threads without materializing it into a container. The only change in the user code is the replacement of access to the object via ->, and not through the dot, for example: earlier from ({1, 2, 3}) . count (), now from ({1, 2, 3}) -> count (). Lastly, Relinx code was transferred to my other project, called nstd , which can be found here .

What for?


This is an eternal and, quite, natural question. “Why, when there is a sea of ​​LINQ-like libraries - take and use?”. Partly, I wrote it because of my own vision of the implementation of such libraries. Partly, due to the desire to use a library that implements LINQ methods to the fullest extent possible, so that if necessary it would be possible to transfer code with minimal changes from one language to another.

Features of my implementation:

  • Using the C ++ 14 standard (in particular, polymorphic lambda expressions)
  • Using adapter iterators only with sequential access (forward-only / input iterators). This allows you to use any types of containers and objects that cannot have random access for various reasons, for example std :: forward_list. This also makes it a little easier to develop custom collection objects that should support std :: begin, std :: end, and iterators themselves should only support operator *, operator! = And operator ++. Thus, by the way, the new for statement for custom types works.
  • A Relinx object is suitable for iterating in a new for statement without conversion to another type of container, as well as in other STL functions-algorithms depending on the type of iterator of the native container.
  • The library implements almost all variants of LINQ methods in one form or another.
  • A Relinx object is as thin as possible over a native collection.
  • The library uses parameter forwarding and implements move semantics instead of copy, where appropriate.
  • The library is fast enough, with the exception of operations that require random access to the elements of the collection (for example, last, element_at, reverse).
  • The library is easily extensible.
  • The library is licensed under the MIT.

Some C ++ programmers do not like iterators and try to somehow replace them, for example with ranges, or do without them at all. But, in the new C ++ 11 standard, in order to support the for statement for custom collection objects, it is necessary to provide iterators (or iterable types, such as pointers) for the for statement. And this requirement is not just STL, but the language itself.

Table of correspondence LINQ methods to Relinx methods:
LINQ methodsRelinx methods
Aggregateaggregate
Allall
 none
Anyany
Asumeumerablefrom
Avarageavarage
Castcast
Concatconcat
Containerscontains
Countcount
 cycle
DefaultIfEmptydefault _ if _ empty
Distinctdistinct
Elementatelement_at
ElementAtOrDefaultelement_at_or_default
Emptyfrom
Exceptexcept
Firstfirst
FirstOrDefaultfirst_or_default
 for_each , for_each_i
Groupbygroup_by
Groupjoingroup_join
Intersectintersect_with
Joinjoin
Lastlast
LastOrDefaultlast_or_default
Long countcount
Maxmax
Minmin
Oftypeof_type
Orderbyorder_by
OrderByDescendingorder_by_descending
Rangerange
Repeatrepeat
Reversereverse
Selectselect, select_i
SelectManyselect_many , select_many_i
Sequencequalsequence_equal
Singlesingle
SingleOrDefaultsingle_or_default
Skipskip
Skipwhileskip_while, skip_while_i
Sumsum
Taketake
TakeWhiletake_while, take_while_i
Thenbythen_by
ThenByDescendingthen_by_descending
Toarrayto_container, to_vector
Todictionaryto_map
Tolistto_list
Tolookupto_multimap
 to_string
Unionunion_with
Wherewhere, where_i
Zipzip

How?


The source code of the library is documented by Doxygen blocks with examples of the use of methods. Also, there are simple unit tests, mostly written by me, to control and match the results of methods execution to C # results. But, they themselves can serve as simple examples of using the library. For writing and testing, I used the MinGW / GCC 5.3.0, Clang 3.9.0 and MSVC ++ 2015 compilers. C MSVC ++ 2015 has problems compiling unit tests. As far as I managed to figure out, this compiler misunderstands some complex lambda expressions. For example, I noticed that if you use the from method inside a lambda, a strange compilation error will crash. There are no such problems with the other compilers listed.

The library is only a header file, which must be included in the module where it will be used.
Before use, for convenience, you can also inject relinx namespace.

A few examples of use:

Simple use. Just calculate the number of odd numbers:

auto result = from({1, 2, 3, 4, 5, 6, 7, 8, 9})->count([](auto &&v) { return !!(v % 2); });
std::cout << result << std::endl;
//Должно быть выведено: 5

An example is more complicated - grouping:

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 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()(i.LastName) ^ (std::hash()(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()("Doe"s) ^ (std::hash()("John"s) << 1)); }).second.size() == 2);
        assert(t2_res->single([](auto &&i){ return i.first == (std::hash()("Doe"s) ^ (std::hash()("Sam"s) << 1)); }).second.size() == 2);

The result of the grouping is a sequence from std :: pair, where first is the key, and second are the Customer elements grouped by this key in the std :: vector container. Grouping by several fields of the same class is done by the hash key in this example, but this is not necessary.

And here is an example of using group_join, which, by the way, does not compile only in MSVC ++ 2015 due to a nested relinx request in lambda expressions themselves:

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 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 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());

In the example, the result of the first operation is the union of two different objects by key using the inner join method, and then grouping them by them.

In the second operation, key joining is performed using the left join method. This is indicated by the last parameter of the method set to true.

And here is an example of using filtering of polymorphic types:

        //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 t1_data = {new derived(), new derived2(), new derived(), new derived(), new derived2()};
        auto t1_res = from(t1_data)->of_type();
        assert(t1_res->all([](auto &&i){ return typeid(i) == typeid(derived2*); }));
        assert(t1_res->count() == 2);
        for(auto &&i : t1_data){ delete i; };




The code can be found here:

GitHub: https://github.com/Ptomaine/nstd , https://github.com/Ptomaine/Relinx

I am ready to answer questions about using the library and I will be very grateful for constructive suggestions for improving the functionality and noticed errors .

Also popular now: