C++ Lambdas & Matrix API

The purpose of this post is to show and explain a bit more about the practical usage of  C++ “lambda power”.  This time in developing a light, fully functional, fast, and hopefully not that often seen, matrix creation utility. Functional programming style.

GitHub gist is here.   The previous post with another very interesting matrix variation is here.

Update 2021 MAR: The gist version 6 is a major update. Matrix related code does not use the std:: lib any more.

Before we proceed:  I do know about Expression Templates and few other optimization techniques. Alas. I do consider them overkill in this context. Another reason for the existence of this post and code is the search for simplicity made possible by C++ lambdas.

This is the core API of your API

You are preparing a “matrix something something” library.  Some game, uni project, PhD project or similar. Let’s say some matrix operations for your specific needs.  Thus I assume you do understand why the core of your library should be something a bit better than T[rows][cols].  I am assuming you need a quick and easy to use, but fully functional, core of your matrix API. The one you can use straight away and concentrate on the functionality you need to provide.  The core API where you actually contain the functionality of keeping the matrices and making them available.

Usage of my single function API is simple.

You need to preserve those matrix dimensions.  Changing the cell value is simple.

The same call is used to both change the value of a matrix cell, or get to the value of the cell.

The whole API is just one function with two arguments: row and column ordinal. How is matrix actually created, is it on the stack or on the heap, you do not need to know, Just use it as the core of your matrix API.

So. Why is “my” solution good? Because it is simple, it is standard C++ and completely decoupled from the std lib. This means the two std lib array and vector are replaced with extremely simple code developed “in house”. This in turn means this matrix solution can be used in mission-critical or embedded or IOT runtime environments.

Let us first study the obvious alternatives. Perhaps they are good enough?

The full std:: lib way

Stack means small(er) matrices, with nothing to worry about, regarding freeing the memory used by the matrix. Also, one can create and use those 2D arrays at compile time.

Let start with using  C++ std:: lib types. For the stack-based matrix, we shall use std::array.

Note: coding style in this part is a bit pedestrian. That is deliberate, it helps to understand.

Above is somewhat equal to int mx_1[2][3]. But here we use std::array  as one very handy and simple std type, for generic stack-based 2D arrays of type T. And unlike native arrays, std::array instances can be moved, can be used with standard templates and algorithms easily.

Heap-based standard solution

Heap is for much larger run-time matrices. But. With the added issue of memory fragmentation. And strict run-time usage. No, compile-time speed.

At first thought, for the heap-based array I decided to use (also c++17 standard) std::unique_ptr<T[]>, basically a smart pointer to an array. Here is a basic, complex but workable solution.

First to note: I made the wrong choice here. The std::vector<T>  is a (much) more elaborate type, but crucially, with the added benefit of giving to the whole mechanism, the crucial ability to be both copied and moved around.  Also, the above can be expanded, made more complex, and involved; if for example, we extend the functionality in order to create a heap-based matrix of any size, or we can create a structure around this core, etc. Or we can think of:

The Alternative

In any case, I am sure you will agree, that is not a very simple looking code. To use it you probably just have to believe, I know what I am doing and that will all work in all sorts of applications you might have.

I somehow do not subscribe to that point of view. Standard C++, 17 and beyond have rapidly increased in complexity. In my opinion, unwarranted complexity. Those two solutions so far, just look too complex to me.

Armed with the C++ core language “Lambda Expressions” feature,  and two small, “base” functions, I might be so bold to present all you need to create and use standard C++ std lib 2D arrays in a very FP fashion.

All the std lib usage is nicely hidden (aka encapsulated) and decoupled inside two tiny lambdas. Without your code knowing or worrying are you on the stack or on the heap. Besides you of course knowing the unique limitations of both kinds of memory.

Without further cogitations, here is the front of the mechanism:

Please take your time, going through the code above.  It is really packed with a lot of modern C++. Showing the “power of lambda”.

NOTE (2021 MAR): As we explained above code is available as the GIST is in version 6.0.0. It is probably faster and simpler because it does not use std::array or std::vector. Thus it can be used in “no exceptions, no streams, no RTTI” projects. That of course does not mean this code is somehow inferior. Please read on.

What surprises most readers, is the capture statement of the lambda above:  [ arry = source_() ]. That is a legal C++.  Lambda capture statement is how users pass lambda object constructor arguments, and name members to be used inside the lambda.

Above we are saying: the result of the call source_() will be copied to the member arry upon constructing the lambda object. Watch carefully. The synopsis of the function object generated by the compiler to implement that lambda is approximately this:

The key point of the above is: when implemented that lambda represents the so-called “local class”.  Let us repeat once more: We return the instance of a local class when returning lambda from inside a function. Here is the solution from above this time with no comments:

And that is the secret sauce. This gives us the greatest degree of encapsulation and decoupling. We use the inner lambda returned on the first call of the outer function.  Inner lambda has access to the closure where it was born. Most importantly to the memory block given to the constructor of the closure where our matrix lambda was born.

Above we do return one arry element by reference. And that lambda is “mutable” which is yet another, secret sauce.

API Sampling

How do we use this in practice? Provided we all understand why is it done as it is. First the inevitable macros. We actually used them at the beginning of this post, without telling you.

As in the gist provided, for the above macros, we need two tiny functions to provide 2D arrays allocated on the stack or on the heap. arr_stack and arr_heap.

Above we have completely encapsulated the mechanism of making arrays and thus decoupled the solution from this mechanism completely.

The commonality of actual types returned above is: operator [ ] (aka indexing) operator and the ability of both to be moved and copied. In case you want to replace them with something else you might fancy in order not to depend on the std lib at all.

Functional programming

The type of the C++ lambda is irrelevant, its functionality is what matters. This is a simple and single API for both 2D array types: stack and heap. Now you can write matrix manipulation functions that are completely decoupled from matrix creation issues.

We simply have a matrix completely encapsulated in a function. The full test, again in an FP style is at the bottom of that gist, already mentioned.

I think this is one very small but useful utility. Showing a lot of good sides of a mixture of modern C++ and functional programming. All simply enabled with lambdas aka closures.

Usability

Using this mechanism we do “carry around” our matrices. Wherever we can copy or move the lambda instance we will take the access of the 2Darray kept inside it. I think you might have already noticed that.

This is giving the ability to pass these matrices by value and also return them by value.  And it all “just works”. Thanks to lambda implementation and thanks to a few key points of this design.

You will quickly find out, to actually use this API, FP style is the best fit. As in the examples above. Which does not mean the OOP style is excluded. It is only, this solution is not suited for OOP. As it is not simple to keep lambda as a member in a class.

Finally

Several measures can be added to stop the abuse, but as it is, this utility can be used in very real, demanding projects. Also, both std::array and std::vector,quality alternative implementations can be found on the net. That is in case your code is running in mission-critical environments.

So little C++ so much good!
So little C++ so much good!