ddv::serial class is a general purpose tool for ordered serial matching of the list of callables against argument passed to serial::operator() of serial::visit().
It has the following capabilities.
-
Serial visitor is constructed from the list of any callables (function pointers, lambdas with or without captures, functor-like class instances, etc) that are stored internally in a tuple.
-
Relation between particular argument
xand callableffalls into one of three categories depending on the invoke result type:- no match: if statement
f(x)orf()results in compile error meaning thatfcannot be called withx, - dynamic match:
f(x)orf()returns an instance ofstd::optional<T>whereTis an arbitrary type, - static match:
f(x)orf()isvoidor returns an instance of some other (non-optional) typeT.
Conclusion from the above: if
fcan be called without arguments, it will be a static or dynamic match for any argumentx. - no match: if statement
-
When
serial::operator()(x)orserial::visit(x)are invoked, callables are probed sequentially in the order of their appearance in the visitor initialization list until a static match is found forx. There could be three possible outcomes.- No match:
xresulted in no match for all callables in the list. Then,operator()(x)will get disabled andvisit(x)will trigger a static assertion (compile error). - Single static match: the first found matched callable
fis a static match. Then,f(x)is called and the result is delivered to the caller. Note that there can be more matches forxafterfin the list, but they will be unreachable and will never be examined in the context of visitingx. - Multiple matches: one or more dynamic matches
g_1, ..., g_kwere found forxbefore the static matchf. Combined, these callables form an effective match chainMforx:M(x) = [g_1, ..., g_k, f]. For the single static match caseM(x) = [f].
The effective match chain is always built at compile time.
- No match:
-
In the latter case of multiple matches, visiting
xworks as following.- For each dynamic match
g_ifromM(x)a callr = g_i(x)is made, where returned instance ofstd::optionalis stored inr. - If
ris initialized, it is returned to the caller and chain processing stops. - Otherwise, the next function from the chain is taken:
i = i + 1, goto step 1. - if a static match
fis reached, it always finishes the chain processing and the result off(x)is returned as a result.
The above algorithms walks through the
M(x)at runtime. - For each dynamic match
-
The value returned from the
serial::operator()(x)to the caller is either:void, ifM(x) = [f]andf(x)is void,- an instance of
std::optional<R>in all other cases, which can be uninitialized.
Let's see how
Ris calculated in different cases.- If
M(x) = [f]andf(x) -> T, thenR = T. - If
M(x) = [g_1, ..., g_k, f]andg_1(x) -> std::optional<T1>, ..., g_k(x) -> std::optional<Tk>, f(x) -> U, thenR = std::variant<distinct(non_void(T1, ..., Tk, U))>. Heredistinct()represents hypothetical compile-time algorithm that removes duplicates from the list of passed types, andnon_void()removesvoidentries from it. - As a specific case of the above, if any
T_ifrom the listT1, ..., Tk, Uisstd::variant<V1, ..., Vn>, then it is replaced withV1, ..., Vnin this list, i.e. variants are replaced with the lists of their alternative types.
The algorithm above calculates the merged result type that can carry the result of invocation of any callable from effective match chain and can be directly initialized like
R(f(x))for anyfinM(x). -
If an argument
xto be matched is an instance ofstd::optionalorstd::variant, it is automatically unpacked and matching is evaluated against the unpacked value. Unpacking is recursive so it can extract from nestedstd::optional<std::variant<...>>types.