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
x
and callablef
falls into one of three categories depending on the invoke result type:- no match: if statement
f(x)
orf()
results in compile error meaning thatf
cannot be called withx
, - dynamic match:
f(x)
orf()
returns an instance ofstd::optional<T>
whereT
is an arbitrary type, - static match:
f(x)
orf()
isvoid
or returns an instance of some other (non-optional) typeT
.
Conclusion from the above: if
f
can 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:
x
resulted 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
f
is a static match. Then,f(x)
is called and the result is delivered to the caller. Note that there can be more matches forx
afterf
in 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_k
were found forx
before the static matchf
. Combined, these callables form an effective match chainM
forx
: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
x
works as following.- For each dynamic match
g_i
fromM(x)
a callr = g_i(x)
is made, where returned instance ofstd::optional
is stored inr
. - If
r
is 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
f
is 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
R
is 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()
removesvoid
entries from it. - As a specific case of the above, if any
T_i
from the listT1, ..., Tk, U
isstd::variant<V1, ..., Vn>
, then it is replaced withV1, ..., Vn
in 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 anyf
inM(x)
. -
If an argument
x
to be matched is an instance ofstd::optional
orstd::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.