.. _cpp_lib_dispatcher: Dynamic Dispatcher ================== A driving philosophy behind SVS is to allow compile-time specialization on as many features as possible, while still providing generic fallbacks. This has an interesting interaction with the type-erasure techniques used throughout the library, namely: * How to we gather the available specializations for a given type-erased together in one place? * Given a collection of argument values, how do we pick the "best" specialization? The solution to this is the ``svs::lib::Dispatcher``. Example with Exposition ----------------------- Motivation ********** We begin with a motivating example. Suppose we have a generic method .. literalinclude:: ../../../../examples/cpp/dispatcher.cpp :language: cpp :start-after: [generic-fallback] :end-before: [generic-fallback] That operates on dynamic types ``a_type`` and ``b_type``, a dynamically determined extent (vector dimension), and takes some type ``Converted`` as the final argument. Next, suppose we can do better if we hoist ``a_type`` and ``b_type`` into the type domain for some select types of interest, and perhaps propagate some static extent as well. So, we define a specialization: .. literalinclude:: ../../../../examples/cpp/dispatcher.cpp :language: cpp :start-after: [specialization-1] :end-before: [specialization-1] Finally, maybe we can do *even* better if we can specialize the final argument on a boolean instead of a string: .. literalinclude:: ../../../../examples/cpp/dispatcher.cpp :language: cpp :start-after: [specialization-2] :end-before: [specialization-2] At this point, we have three methods: a generic fallback and two specialized templates. In our final program, we cannot instantiate our specialized implementations for all possible types and values as that either would bloat the final binary significantly or simply be unfeasible. In an ideal world, we could define a list of selected specializations in a centralized location, invoke those when our run-time parameters match those specializations, and invoke the fallback otherwise. This is where the ``svs::lib::Dispatcher`` comes into play. Dispatcher ********** .. literalinclude:: ../../../../examples/cpp/dispatcher.cpp :language: cpp :start-after: [dispatcher-definition] :end-before: [dispatcher-definition] Above, we see the definition of a ``std::variant`` (corresponding to the final ``std::string`` or ``bool`` arguments of our specializations. Next, there is the definition of a dispatcher taking four arguments and returning ``void``. With this dispatcher, we can register our specializations and generic fallback: .. literalinclude:: ../../../../examples/cpp/dispatcher.cpp :language: cpp :start-after: [register-methods] :end-before: [register-methods] In the above snippet, we see target registration by passing a reference to the fully-instantiated specializations and a single instance of the generic method. .. NOTE:: Passing C++ functions by reference is an acceptable way to pass the desired dispatch target. Mechanically, a method representing the full specialization will be compiled and the function reference will decay to a function pointer to this specialization. It is also acceptable to pass a lambda directly by value. When passing a lambda, it is crucial to ensure that any value captured by reference properly outlives the life of the dispatcher. Upon registration, SVS will check that all source argument types of the dispatcher are convertible to the argument types of the target by checking for a specialization of ``svs::lib::DispatcherConverter``. The converter defines match suitability (whether a conversion from source value to destination type is possible and if so, how "good" that conversion is) and the actual argument conversion. SVS already contains rules for converting ``svs::DataType`` to ``svs::lib::Type``, rules for recursively matching ``std::variant`` types, and conversions between different applicable reference qualifiers of the same type. To hook in the custom ``Converted`` type into this system, we can define our own conversion rules: .. literalinclude:: ../../../../examples/cpp/dispatcher.cpp :language: cpp :start-after: [converted-dispatch-conversion-rules] :end-before: [converted-dispatch-conversion-rules] With these rules defined, SVS fill figure out how to convert each alternative in the variant into a ``Converted`` if needed. Example Runs ************ Now, a main function can be defined that parses commandline arguments into the dispatch types and invokes the overload resolution logic in the dispatcher. .. literalinclude:: ../../../../examples/cpp/dispatcher.cpp :language: cpp :start-after: [main] :end-before: [main] Possible runs may look like this: :: Input: float16 float16 128 false hello Output: Generic: float16, float16, dynamic with arg "hello" Input: float16 float16 128 false true Output: Generic: float16, float16, dynamic with arg "boolean true" Input: float16 float16 128 true true Output: ANNException (no match found) Input: uint32 uint8 128 true hello Output: Specialized with string: uint32, uint8, 128 with arg "hello" Input: float32 float32 100 false false Output: Specialized with flag: float32, float32, dynamic with arg "false" Automatic Documentation Generation ********************************** One advantage of grouping all methods together in a single place is that we can use the documentation feature of the dispatcher to describe all registered methods. The code for the help message for our example executable is show below .. literalinclude:: ../../../../examples/cpp/dispatcher.cpp :language: cpp :start-after: [print-help] :end-before: [print-help] The generated help message might look something like this: :: Registered Specializations -------------------------- { type A, type B, Extent, Last Argument } { float32, float32, any, all values -- (union alternative 1) } { float32, float32, any, all values -- (union alternative 0) } { uint32, uint8, 128, all values -- (union alternative 1) } { all values, all values, any, all-boolean-values OR all-string-values -- (union alternatives 0, 1) } API Documentation ----------------- Classes ******* .. doxygenclass:: svs::lib::Dispatcher :project: SVS :members: .. doxygenclass:: svs::lib::DispatchTarget :project: SVS :members: Dispatch API ************ .. doxygenstruct:: svs::lib::DispatchConverter :project: SVS .. doxygenconcept:: svs::lib::DispatchConvertible :project: SVS .. doxygenfunction:: svs::lib::dispatch_match :project: SVS .. doxygenfunction:: svs::lib::dispatch_convert :project: SVS .. doxygenfunction:: svs::lib::dispatch_description :project: SVS .. doxygenvariable:: svs::lib::dispatcher_build_docs :project: SVS .. doxygenvariable:: svs::lib::dispatcher_no_docs :project: SVS Predefined Scores ***************** .. doxygenvariable:: svs::lib::invalid_match :project: SVS .. doxygenvariable:: svs::lib::perfect_match :project: SVS .. doxygenvariable:: svs::lib::imperfect_match :project: SVS .. doxygenvariable:: svs::lib::implicit_match :project: SVS Helpers ******* .. doxygenconcept:: svs::lib::ImplicitlyDispatchConvertible :project: SVS .. doxygenstruct:: svs::lib::variant::VariantDispatcher :project: SVS :members: Full Example ------------ The full example described at the beginning is given below. .. literalinclude:: ../../../../examples/cpp/dispatcher.cpp :language: cpp :start-after: [example-all] :end-before: [example-all]