c++ metacall ™ dispatcher

Several years down the line and finally here is the Godbolt explaining it all. Hopefully.

The key change is in the name: Dispatcher instead of a “CallStream”. Read on and you will understand why is that a much more fitting name. Below is a very long and confusing post. So for the time being this post is W.I.P.The old name is left in, to keep the original text intact.

2019 JUN 28

CallStream Part 3 : C++ Binding

Good old C++ is one strongly typed language. CallStream concept is born (and bred) in the domain of type-less JavaScript. Also, C++ is (so-called) static (compiled) language, vs dynamic (interpreted) JavaScript. C# is somewhere in between, (but not in the middle !) especially in its latest 4.0 reincarnation. Still, I think CallStream should be implemented in C++ too. Why?

Because CallStream is not an implementation idiom of a programming language. It is a programming concept. And the concept is not very good if it is not universally applicable. In this case, it is not ubiquitous, and therefore not used everywhere by everyone. Not successful, in one word.

In the case of C++, I have decided to make a (sort of a) type-less CallStream. A C++ interfacing mechanism that allows for more or less the same call-streaming as in JavaScript. Where one can “call” anything with any arguments. Here is the main from that Godbolt link on the top:

Above is the main(), of the real code that compiles and runs. (the Godbolt link is on the top). This CallStream implementation allows calls where the first argument can be a string literal, function, intrinsic type, or object (aka class instance). With any number of any type of arguments following. Let’s jump straight to the CallStream type.

This is one CallStream class, serving the needs of all. Calls are just streaming in.

How is that possible in C++?

The “trick” is in the generic function call operator. As implemented inside this non-generic class. Actually (at the moment) two templated “operator ()” overloads, and one specialization1, appear to be sufficient. Where each opearator (), has a simple task to pass the first argument and va_list pointer to the dbj::bridge() function. Where the real processing happens. The application space, that I am calling “behind”.

So, what is happening here? The compiler dispatches all the calls from the main() example above to the CallStream instance, one of these three function call operators. Each of them calls one of the (potentially) many bridge functions available. The “bridge” in this context means the same as in the CallStream concept: a “bridge” to the implementation. I am also using the term “bridge” because this implementation hiding concept, reminds me (somewhat) of the “bridge pattern” as described by Gof4.

Which bridge function is going to be called is decided by the c++ compiler, following the usual c++ rules of function overloading. A real “c++ kabala” when we mix in overloaded, with generic with specialized templated “bridges”.

Here is the generic bridge function and a handful of basic specializations of it.

What we have till now is enough to call stream in c++. All calls where the first argument is not int, bool, char, or string literal will be passed to the generic dbj::bridge defined above. Let’s try.

Basically, function call operators are called and from this, the bridge overload is found and called, with the variable number of arguments given.

The last call above is passing the c++ iostreams library std::cout object as the first parameter to the call. The compiler realizes there is no bridge specialized for handling this type (std::ostream), and thus uses the generic bridge, defined above.

Detour: Better design?

One could imagine an “OO solution” based on inheritance, where only one generic bridge is used which somehow “cleverly” dispatches calls to their final destinations. That would inevitably require looking into the concrete type of the first call parameter, thus quite slow and cumbersome. Also, a solution is not very resilient to change. It will be in essence one huge switch to be frequently updated as new types have to be handled by the CallStream. And of course, this single generic bridge+dispatcher will be very difficult to maintain and re-use as a library solution. It is much better to use a c++ compiler to find the specialized bridge, that you have “built” (inside your own code) so that CallStream can pass the appropriate calls to the particular implementation you want to place “behind” a CallStream mechanism.

This is all solvable as above with templates and specializations.  For a “classical OO” solution, consider some useless but true alternative:

We can easily imagine a solution based on a hierarchy of concrete CallStreams inheriting from that base. This will generate much more c++ code, and again a solution that will require inheriting from the core mechanism for each new type to be handled. A classical c++ nightmare where there is a huge logical hierarchy, physically scattered over many applications, with the root of it being in some single library. The root which therefore has to be “Cast in blood and signed in stone” is never ever to be changed. Also above there is no implementation, only required behavior. Solutions such as the one above do generate redundant implementations “all over the place”. I am glad to notice that once more inheritance is rendered as not good, for a solution to be used in real-life situations.

Back to Better

Now, back to our shiny solution based on a single CallStream type and concrete bridge overload defined by “customers” for their specific purposes.

Imagine we have a customer which wants to hide her complex “things” behind a CallStream and thus make her solution look nice and easy and eminently usable. Accidentally the same customer is keen on functors, How would that customer quickly subscribe to the CallStream concept and implement a small layer of code necessary?

First, a simple functor has to be invented to act as a primary type passed into call streams and used to create a bridge specialization that will be used by the c++ compiler, to pass the calls and parameters to the code “behind”.

CallStream need not be changed for this (or any other) customer to be usable. And customers specialized “bridge function” to be found and used. In this example, everything is in the same namespace (dbj), while in reality Functor above will be in a separate namespace. Again we could imagine and present a solution where Functor will not deal with va_list‘s. That would require a more complex bridge to decode the parameters etc. Instead, I have made a solution with templatized functors, who “know” what type and value they are dealing with. For example:

Since the customer is using those functors here, we could pass some useful data inside functors instances too.

Buy now we are encroaching into the customer’s domain. The code that was necessary to use (or be used by) CallStream is short and sweet. And it is perfectly applicable for anyone using functors with CallStream.

One more example and that is it. This “post” is already turning into a “booklet” ;) This time I will pay attention to CallStream customers who are at the same time perhaps implementing some library in the (right) spirit of c++ std::: a lot of generic functions and the whole solution in header files. Still, they want to have a “CallStream enabled” interfacing with their pre-existing library. Let’s keep this one simple but still valid c++.

That is the whole library. And here is the adaptor code required so that CallStream users can use that lib through the call streaming.

Yes, just this one specialized bridge is enough for the CallStream mechanism to find it and to pass calls to the “ops” library operations.

Also, just to confirm the CallStream malleability, if one passes the ‘ops’ operation which is not having its bridge coded yet, the code will still work, it is only that the generic bridge will be called. Of course, the CallStream instance above is valid for passing to it any calls. It is the same as before. It will find any specialized bridge in the scope, to dispatch these calls to their intended implementations.

Conclusion

All these C++ “snazzy” idioms, are not here because of my “requirements” or dictate. I am showing all these used with/by CallStream to confirm the eligibility of the whole concept implemented using C++.


1Or template concretization, as I am calling it. And a few others, too.