📜 ⬆️ ⬇️

Relinx is another implementation of .NET LINQ methods in C ++, with support for lazy evaluation.

RelinxLogo
(UPDATED!)
Among the many implementations of LINQ-like libraries in C ++, there are many interesting, useful and efficient ones. But in my opinion, most of them are written with a certain disregard for C ++ as a language. All the code of these libraries is written as if they are trying to correct its "ugliness." I admit, I love C ++. And as if he was not sling mud, my love for him will hardly pass. Perhaps this is partly because this is my first high-level programming language and the second one I learned after Assembler.



Important update : A 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 the relinx_object in heap memory and control the life cycle of the entire chain of transformations. Now std :: shared_ptr <relinx_object> can be passed to functions and threads without its materialization in the container. The only change in the user code is to replace access to the object through ->, and not through a period, for example: earlier from ({1, 2, 3}) . count (), now from ({1, 2, 3}) -> count (). And finally, Relinx code has been 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 it and use it?”. In part, I wrote it because of my own vision of the implementation of such libraries. Partly, because of the desire to use the library, which implements LINQ methods as fully as possible, so that, if necessary, you can transfer the code with minimal changes from one language to another.
')
Features of my implementation:


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 user collection objects, it is necessary to provide for the for statement exactly iterators (or iterable types, for example, pointers). And this requirement is not just STL, but the language itself.

Correspondence table LINQ methods Relinx methods:
LINQ methodsRelinx methods
Aggregateaggregate
Allall
none
Anyany
AsEnumerablefrom
Avarageavarage
Castcast
Concatconcat
Containscontains
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
Longcountcount
Maxmax
Minmin
OfTypeof_type
Orderbyorder_by
OrderByDescendingorder_by_descending
Rangerange
Repeatrepeat
Reversereverse
Selectselect, select_i
SelectManyselect_many , select_many_i
SequenceEqualsequence_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 library source code is documented by Doxygen blocks with examples of using methods. Also, there are simple unit tests, mostly written by me to control and match the results of the execution of the methods to C #. 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. With MSVC ++ 2015, there are problems compiling unit tests. As far as I was able 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. With other listed compilers there are no such problems.

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

A few examples of use:

Simple use. Simply count 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<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); 

The result of the grouping is a sequence from std :: pair, where first is the key, and second is 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, is not compiled only in MSVC ++ 2015 due to the nested relinx query in the 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<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()); 

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

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

And here is an example of the use of filtering 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<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; }; 




The code can be found here:

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

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

Source: https://habr.com/ru/post/303538/


All Articles