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
- Part One: Call Stream meta pattern
- Part Two: Call Stream C# Binding
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
/* */ int main(int argc, const char * argv[]) { dbj::Functor & gf = dbj::GFunctor<int>(); /* the call stream instance */ dbj::CallStream & cs = dbj::CallStream() ; /* Single instance of a single CallStream type can be used for a large variety of different c++ calls all metacalling into the one mechanism */ cs ("add",1,2) (dbj::ops::add<int>,3,5) (dbj::ops::mul<int>,4,6) (true) (gf,6); } |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
/* CallStream is a single non-generic type. It declares overloaded generic function call operators It depends on the existence of the family of 'bridge' functions At compile time first appropriate 'operator ()' is found and used on the instance of this class From inside 'operator ()' found, second an appropriate bridge is found and used to the concrete implementation 'behind' if there is no required bridge function specialization found there is always a generic one. Result is comprehensive decoupling between callers and implementations hidden behind a call stream. */ class CallStream { public: /* generic 'operator ()' for pointers and variable number of arguments following */ template<typename T> const CallStream & operator () (const T * f, ...) const { va_list vl; va_start( vl, f ); dbj::bridge( *f, vl ); va_end(vl); return *this ; } /* generic 'operator ()' for references and variable number of arguments following */ template<typename T> const CallStream & operator () (const T & f, ...) const { va_list vl; va_start( vl, f ); dbj::bridge( f, vl ); va_end(vl); return *this ; } /* concretized 'operator ()' for string literals and variable number of arguments following */ template<> const CallStream & operator () (const char * s, ...) const { va_list vl; va_start( vl, s ); dbj::bridge( s, vl ); va_end(vl); return *this ; } } ; |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
namespace dbj { /* CallStream depends on the existence of this function. it dispatches the calls received to the concretized bridge function as found by compiler. if there is no required bridge function specialization found there is always a generic one. this is the root of the whole family of bridge functions */ template<typename T> inline void bridge (const T & t_ , va_list & vl ) { std::cout << "nGeneric bridge used for type" << typeid(t_).name() ; } /* Bridge specializations. If compiler does not find a specialized bridge it needs it will call the generic bridge above. The int bridge specialization */ template<> inline void bridge<int> (const int & j, va_list & /*vl*/) { std::cout << "nGeneric bridge used for 'int' type, value: " << j ; } /* literal strings bridge */ inline void bridge (const char * s, va_list & /*vl*/ ) { std::cout << "nGeneric bridge used for 'char *' type, value: " << s ; } /* char's bridge */ template<> inline void bridge<char> ( const char & c, va_list & /*vl*/) { std::cout << "nGeneric bridge used for 'char' type, value: " << c ; } /* bool's bridge */ template<> inline void bridge<bool> (const bool & b, va_list & /*vl*/) { std::cout << "nGeneric bridge used for 'bool' type, value: " << b ; } } // dbj |
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.
1 2 3 4 5 6 7 8 |
// // first create an instance of the call stream type dbj::CallStream & cs = dbj::CallStream() ; cs ("add",1,2) // that goes to overloaded bridge for handling string literal ('X',1,2,3) // to specialized bridge for handling char (7,6,8,9) // to specialized bridge for handling int (true,11) // to specialized bridge for handling bool (std::cout,"this","goes","to","generic","bridge"); |
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:
1 2 3 4 5 6 7 8 9 10 11 12 |
/* */ namespace lib { /* Alternative design: this is an abstract base of all CallStreams */ class CallStreamBase { public: /* 'operator ()' for type pointer*/ template<typename T> const CallStreamBase & operator ()(const T * f, ...) const = 0 ; /* 'operator ()' for type reference */ template<typename T> const CallStreamBase & operator ()(const T & f, ...) const = 0 ; }; } |
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”.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
/* Abstract base to all functors specific to the following example */ class Functor { protected std::string tag_ ; public: /* in this example each functor inheritor must have an 'operator (va_list &)' implemented */ virtual void operator ()( va_list & vl ) const = 0 ; /* method to show functor type and value held using std::cout by default */ virtual void show ( std::ostream & out_ = std::cout ) const = 0 ; }; /* The CallStream bridge specialization that will reach to any kind-of-a Functor above */ template<> inline void bridge<Functor> (const Functor & gf, va_list & vl) { std::cout << std::endl << "Bridge to the " << typeid(gf).name(); gf(vl); // passing to the functor variable number of arguments gf.show() ; // showinf the type and value held } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
/* The generic functor which deals with different types of arguments sent through call stream. */ template<typename T> class GFunctor : public Functor { public: typedef T ValueType; typedef GFunctor<T> MyType ; typedef GFunctor<T> * MyTypePtr ; protected : T value_ ; MyType & This () const { return * const_cast<MyTypePtr>(this);} public: GFunctor( const char * tag ) { if ( tag ) This().tag_ = strdup(tag) ; else This().tag_ = "" ; } /*This functor requires exactly one argument of type T*/ virtual void operator ()( va_list & vl ) const { This().value_ = va_arg( vl, T ); } virtual void show ( std::ostream & out_ = std::cout ) const { out_ << "nFunctor of type " << typeid(*this).name() << ", is holding the type " << typeid(T).name() << ", and the value is: " << value_ ; } }; |
Since the customer is using those functors here, we could pass some useful data inside functors instances too.
1 2 3 4 5 6 7 8 9 10 11 12 |
const dbj::Functor & gf = dbj::GFunctor<int>("LABEL"); /* make a functor to deal with single int's for a particular customer */ const dbj::CallStream & cs = dbj::CallStream() ; /*Again the same single CallStream is used */ cs (gf,9) // "LABEL" and 9 are available to the implementation (gf,6); // "LABEL" and 6 |
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++.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
/* the whole 'ops' library is made out of inline generic functions and no specializations */ namespace ops { /* generic functions to add or multiply "anything" that has binary '+' and '*' operators */ template<typename T> inline T add( const T & a, const T & b) { return a + b ; } template<typename T> inline T mul( const T & a, const T & b) { return a * b ; } /* int_int_int is a function pointer type to a concretized form of generic ops above which are concretized with an int type */ typedef int ( * int_int_int ) ( const int &, const int & ) ; } |
That is the whole library. And here is the adaptor code required so that CallStream users can use that lib through the call streaming.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
namespace dbj { /*This bridge will be called for all the ops generic functions which are concretized as dealing with int's CallStream need not be changed for this bridge to be found and used. */ inline void bridge (const ops::int_int_int operation, va_list & vl ) { int op1 = va_arg( vl, int ); int op2 = va_arg( vl, int ); int rez = operation(op1,op2) ; std::cout << std::endl << "Bridge to the " << typeid(operation).name() << " result of operation(" << op1 << "," << op2 << ") is : " << rez ; } } // namespace dbj |
Yes, just this one specialized bridge is enough for the CallStream mechanism to find it and to pass calls to the “ops” library operations.
1 2 3 4 5 6 7 8 9 |
int main(int argc, const char * argv[] ) { const dbj::CallStream & cs = dbj::CallStream() ; cs (ops::add<int>,3,5) (ops::mul<int>,4,6) (ops::add<float>,2.3,7.12) ; // works but generic bridge is called } |
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.