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

struct Converted {
    std::string value_;
};
void generic(
    svs::DataType a_type,
    svs::DataType b_type,
    svs::lib::ExtentTag<svs::Dynamic> SVS_UNUSED(extent_tag),
    Converted converted
) {
    fmt::print(
        "Generic: {}, {}, {} with arg \"{}\"\n",
        a_type,
        b_type,
        format_extent(svs::Dynamic),
        converted.value_
    );
}

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:

// A specialized method.
// SVS defines the dispatch conversion from `svs::DataType` to `svs::lib::Type`.
// This overload takes an additional `std::string` argument type.
template <typename A, typename B, size_t N>
void specialized(
    svs::lib::Type<A> a_type,
    svs::lib::Type<B> b_type,
    svs::lib::ExtentTag<N> SVS_UNUSED(extent_tag),
    const std::string& arg
) {
    // Convert `svs::lib::Type` to `svs::DataType`.
    svs::DataType a = a_type;
    svs::DataType b = b_type;
    fmt::print(
        "Specialized with string: {}, {}, {} with arg \"{}\"\n", a, b, format_extent(N), arg
    );
}

Finally, maybe we can do even better if we can specialize the final argument on a boolean instead of a string:

template <typename A, typename B, size_t N>
void specialized_alternative(
    svs::lib::Type<A> a_type,
    svs::lib::Type<B> b_type,
    svs::lib::ExtentTag<N> SVS_UNUSED(extent_tag),
    bool flag
) {
    // Convert `svs::lib::Type` to `svs::DataType`.
    svs::DataType a = a_type;
    svs::DataType b = b_type;
    fmt::print(
        "Specialized with flag: {}, {}, {} with arg \"{}\"\n", a, b, format_extent(N), flag
    );
}

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

// A variant of types given for the last argument of the methods we wish to dispatch to.
using Variant = std::variant<bool, std::string>;

// A dispatcher wrapping and dispatching to functions that return void and whose arguments
// are constructible using dispatcher conversion from the remaining types.
using Dispatcher =
    svs::lib::Dispatcher<void, svs::DataType, svs::DataType, svs::lib::ExtentArg, Variant>;

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:

Dispatcher build_and_register() {
    // Default construction for a dispatcher.
    auto dispatcher = Dispatcher{};

    // When registering methods, argument documentation can be requested using this tag.
    // Note dispatch rules are not required to implement documentation, in which case
    // default documentation will be provided.
    constexpr auto build_docs = svs::lib::dispatcher_build_docs;

    // Register the desired specializations.
    dispatcher.register_target(build_docs, &specialized<float, float, svs::Dynamic>);
    dispatcher
        .register_target(build_docs, &specialized_alternative<float, float, svs::Dynamic>);
    dispatcher.register_target(build_docs, &specialized<uint32_t, uint8_t, 128>);

    // Register the dynamic fallback.
    dispatcher.register_target(build_docs, &generic);
    return dispatcher;
}

const Dispatcher& get_dispatcher() {
    // Only allocated and populate the dispatcher once.
    static Dispatcher dispatcher = build_and_register();
    return dispatcher;
}

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:

// Define full-specializations for converting `std::string` and `bool` to `Converted`.
template <> struct svs::lib::DispatchConverter<std::string, Converted> {
    // Return ``true`` if this is a match. Otherwise, return false.
    //
    // To provide finer-grained control, an ``int64_t`` can be returned instead, where
    // negative values indicate an invalid match (a method that has an invalid match for
    // any argument must be discarded), and positive values indicate degree of matches
    // with lower numbers having higher priority.
    static bool match(const std::string& SVS_UNUSED(arg)) { return true; }

    // This argument is called when a method has been selected and we're ready to convert
    // the source argument to the destination argument and invoke the registered target.
    static Converted convert(const std::string& arg) { return Converted{arg}; }

    // Provide documentation regarding the values accepted by this conversion.
    static std::string_view description() { return "all-string-values"; }
};

template <> struct svs::lib::DispatchConverter<bool, Converted> {
    static bool match(bool SVS_UNUSED(arg)) { return true; }
    static Converted convert(bool arg) { return Converted{fmt::format("boolean {}", arg)}; }
    static std::string_view description() { return "all-boolean-values"; }
};

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.

int svs_main(const std::vector<std::string>& args) {
    // Perform some very basic
    const size_t nargs = args.size();
    bool requested_help = std::any_of(args.begin(), args.end(), [](const auto& arg) {
        return arg == "--help" || arg == "help";
    });
    if (nargs != 6 || requested_help) {
        print_help();
        return 0;
    }

    // Parse the argument types.
    svs::DataType type_a = parse_datatype(args.at(1));
    svs::DataType type_b = parse_datatype(args.at(2));
    svs::lib::ExtentArg extent_arg = parse_extent_arg(args.at(3), parse_bool(args.at(4)));

    // Construct a variant according to the value of `arg`.
    const auto& arg = args.at(5);
    auto maybe_bool = parse_bool_nothrow(arg);
    auto variant = [&]() -> Variant {
        if (maybe_bool) {
            return maybe_bool.value();
        }
        return arg;
    }();

    // Instantiate the dispatcher and dispatch to the best fitting method.
    get_dispatcher().invoke(type_a, type_b, extent_arg, variant);
    return 0;
}

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

void print_help() {
    constexpr std::string_view help_template = R"(
Usage:
    (1) dispatcher type_a type_b dims enforce_dims arg
    (2) dispatcher --help

1. Run the dispatcher example.
   * type_a and type_b: must be parseable as s `svs::DataType`.
   * dims: The number of dimensions to dispatch on. Can either be an integer or the string
     "dynamic"
   * enforce_dims: Whether or not relaxation to dynamic dimensionality is allowed. Must
     either be "true" or "false"
   * arg: An additional string argument. If arg is either "true" or "false", it will be
     parsed as a boolean. Otherwise, it will remain as a string and be forwarded to the
     appropriate overload.

2. Print this help message.

Registered Specializations
--------------------------
{{ type A, type B, Extent, Last Argument }}

{}
)";
    const auto& dispatcher = get_dispatcher();
    auto method_docs = std::vector<std::string>();
    constexpr size_t nargs = Dispatcher::num_args();
    for (size_t i = 0, imax = dispatcher.size(); i < imax; ++i) {
        auto arg_docs = std::vector<std::string>();
        for (size_t j = 0; j < nargs; ++j) {
            arg_docs.push_back(dispatcher.description(i, j));
        }
        method_docs.push_back(fmt::format("{{ {} }}", fmt::join(arg_docs, ", ")));
    }

    fmt::print(help_template, fmt::join(method_docs, "\n"));
}

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

template<typename Ret, typename ...Args>
class Dispatcher

A dynamic, multi-method dispatcher for registering specializations.

Multiple target methods can be registered with the dispatcher, provided that each target method has the same number of arguments and dispatch conversion between each target argument type and its corresponding member in Args is defined.

When invoked, the dispatcher will find the most applicable registered target by applying svs::lib::dispatch_match on its arguments and the argument types of each registered method.

The most specific applicable method will then be invoked by calling svs::lib::dispatch_convert on each argument to its corresponding target type.

Template Parameters:
  • Ret – The return type of invoking ai contained method.

  • Args – The run-time arguments to dispatch over.

Public Types

using match_type = std::array<int64_t, sizeof...(Args)>

The type used to represent method matches for scoring.

Public Functions

Dispatcher() = default

Construct an empty dispatcher.

inline size_t size() const

Return the number of registered candidates.

template<typename F>
inline void register_target(F f)

Register a callable with the dispatcher.

template<typename F>
inline void register_target(BuildDocsTag build_docs, F f)

Register a callable with the dispatcher with conversion documentation.

inline std::pair<std::optional<size_t>, match_type> best_match(const std::remove_cvref_t<Args>&... args) const

Get the index and the score of the best match.

If no match is found, then the optional in the return value will be empty. In this case, the contents of the match_type is undefined.

inline bool has_match(const std::remove_cvref_t<Args>&... args) const

Return whether or not the given collection of arguments can be matched with any method registered in the dispatcher.

inline Ret invoke(Args... args) const

Invoke the best matching method and return the result.

inline std::string description(size_t method, size_t argument) const

Return dispatch documentation for the given method and argument.

Throws an svs::ANNException if method >= size() or argument >= num_args().

Public Static Functions

static inline constexpr size_t num_args()

Return the number of arguments the dispatcher expects to receive.

template<typename Ret, typename ...Args>
class DispatchTarget

Method wrapper for the target of a dispatch operation.

Template Parameters:
  • Ret – The return type of the method.

  • Args – The dispatch argument types of the method.

Public Types

using signature_type = detail::Signature<Ret(Args...)>

The full signature of the dispatch arguments.

using arg_signature_type = detail::Signature<void(Args...)>

The signature of the dispatch objects without a return type.

using match_type = std::array<int64_t, num_args>

The type encoding a match based on dispatch values.

Public Functions

template<typename Callable>
inline DispatchTarget(NoDocsTag tag, Callable f)

Construct a DispatchTarget around the callable f with no documentation.

The following requirements must hold:

  1. The argument types of f must be deducible (in other words, f cannot have an overloaded call operator nor can its call operator be templated).

  2. The number of arguments of f must match Dispatcher::num_args.

  3. Furthermore, dispatch conversion must be defined between each dispatch argument and its corresponding argument in f.

If any of these requirements fails, this method should not compile.

Parameters:
  • tag – Indicate no argument conversion documention is required.

  • f – The function to wrap for dispatch. The wrapped functor must have a const-qualified call operator and no non-const-qualified call operator.

template<typename Callable>
inline DispatchTarget(BuildDocsTag tag, Callable f)

Construct a DispatchTarget around the callable f with documentation.

The following requirements must hold:

  1. The argument types of f must be deducible (in other words, f cannot have an overloaded call operator nor can its call operator be templated).

  2. The number of arguments of f must match Dispatcher::num_args.

  3. Furthermore, dispatch conversion must be defined between each dispatch argument and its corresponding argument in f.

If any of these requirements fails, this method should not compile.

Parameters:
  • tag – Indicate argument conversion documention is required.

  • f – The function to wrap for dispatch. The wrapped functor must have a const-qualified call operator and no non-const-qualified call operator.

inline match_type check_match(const std::remove_cvref_t<Args>&... args) const

Return the result of matching each argument with the wrapped method.

inline Ret invoke(Args&&... args) const

Invoke the wrapped method by dispatch-converting each argument.

inline std::string description(size_t i) const

Return dispatch documentation for argument i.

If i >= num_args, throws svs::ANNException indicating a bounds error. If the DispatchTarget was constructed without documentation, then this function returns the string “unknown”>

Public Static Attributes

static constexpr size_t num_args = sizeof...(Args)

The number of arguments this method accepts.

Dispatch API

template<typename From, typename To>
struct DispatchConverter : public std::false_type

Customization point for defining dispatch conversion rules.

Expected API:

template<> struct DispatchConverter<From, To> {
    // Return a score for matching arguments of type `From` to type `To`.
    // * Negative values indicate an invalid match (cannot convert).
    // * Non-negative values are scored with lower values given higher priority.
    static int64_t match(const std::remove_cvref_t<From>&);

    // Perform a dispatch conversion.
    // This behavior of this function is undefined if `match` returns an invalid score.
    static To convert(From);

    // An optional method describing the acceptable value for this conversion.
    static std::string description();
}

Note that specialization requires full cv-ref qualification of the From and To types in order to be applicable.

template<typename From, typename To>
concept DispatchConvertible
#include <svs/lib/dispatcher.h>

Concept indicating whether a specialization of DispatchConverter e xists for this combination of arguments - implying that dispatcher conversion is well-defined.

template<typename From, typename To>
constexpr int64_t svs::lib::dispatch_match(const std::remove_cvref_t<From> &x)

Return the matching score of an instance of type From to the type To.

All non-negative results should be considered with a lower number having a higher priority. So 0 has the highest priority, followed by 1, then 2 etc.

Internally, this method calls svs:lib::DispatchConverter<From, To>::match and forward the result. However, if the DispatchConverter returns a bool, then the return value will be converted to the canonical int64_t representation appropriately.

Parameters:

x – The value being dispatched on.

Returns:

An signed integer score. Scores less than 0 imply invalid match and the entire method being considered should be discarded.

template<typename From, typename To>
constexpr To svs::lib::dispatch_convert(From &&x)

Use dispatch conversion to convert a value of type From to To.

It is undefined behavior to call this method if svs::lib::dispatch_match<From, To>(x) is invalid.

template<typename From, typename To>
auto svs::lib::dispatch_description() -> std::string

Return documentation for a dispatch conversion, if available.

If Such conversion is not available (svs::lib::DispatchConverter<From, To>::description() was not defined), then returns the sentinel string “unknown”.

constexpr BuildDocsTag svs::lib::dispatcher_build_docs = {}

Tag to build argument-conversion documentation.

constexpr NoDocsTag svs::lib::dispatcher_no_docs = {}

Tag to suppress argument-conversion documentation.

Predefined Scores

constexpr int64_t svs::lib::invalid_match = -1

The worst possible invalid match.

constexpr int64_t svs::lib::perfect_match = 0

The best possible match.

constexpr int64_t svs::lib::imperfect_match = 1

The next best possible match.

constexpr int64_t svs::lib::implicit_match = 10000

Match was found using an implicit conversion.

This conversion is performed on arguments with the same type (or ref-compatible types). Use a non-zero value to allow specializations to provide better matches than the type identity.

Helpers

template<typename From, typename To>
concept ImplicitlyDispatchConvertible
#include <svs/lib/dispatcher.h>

Two types are considered dispatcher convertible if:

  • Removing cv-ref qualifiers from From and To yields the same type.

  • From and be forwarded to To without invoking a copy constructor.

For a type non-reference type T the following implicit conversion are allowed:

  • T -> T (using move construction), T -> const T&, T&& -> const T&

  • T& -> T&, T& -> const T&

  • const T& -> const T&

  • T&& -> T, T&& -> const T&, T&& -> T&&

template<DispatchCategory Cat, typename To, typename ...Ts>
struct VariantDispatcher

Match all applicable alternatives of a variant to the destination type.

Public Static Functions

static inline constexpr int64_t match(const std::variant<Ts...> &x)

Match the current alternative in the variant to To.

If an alternative type is not svs::lib::DispatchConvertible with To, then return svs::lib::invalid_match.

static inline constexpr To convert(variant_type x)

Dispatch convert the current alternative in the variant to the type To.

Throws svs::ANNException if such a conversion is undefined.

static inline std::string description()

Document all possible conversion from the variant to To.

Full Example

The full example described at the beginning is given below.

#include "svs/lib/dispatcher.h"
#include "svs/third-party/fmt.h"

#include "svsmain.h"

#include <string_view>

namespace {

std::optional<bool> parse_bool_nothrow(std::string_view arg) {
    if (arg == "true") {
        return true;
    }
    if (arg == "false") {
        return false;
    }
    return std::nullopt;
}

/// Parse a string as a boolean.
///
/// Throws a ``svs::ANNException`` if parsing fails.
bool parse_bool(std::string_view arg) {
    auto v = parse_bool_nothrow(arg);
    if (!v) {
        throw ANNEXCEPTION(
            "Cannot parse \"{}\" as a boolean value! Expected either \"true\" or "
            "\"false\".",
            arg
        );
    }
    return v.value();
}

/// Parse a string as a valid ``svs::DataType``.
svs::DataType parse_datatype(std::string_view arg) {
    auto type = svs::parse_datatype(arg);
    if (type == svs::DataType::undef) {
        throw ANNEXCEPTION("Cannot parse {} as an SVS datatype!", type);
    }
    return type;
}

/// Parse a string as an extent.
svs::lib::ExtentArg parse_extent_arg(const std::string& extent, bool enforce) {
    if (extent == "dynamic") {
        return svs::lib::ExtentArg{svs::Dynamic, enforce};
    }
    // Try to parse as an integer.
    return svs::lib::ExtentArg{std::stoull(extent), enforce};
}

std::string format_extent(size_t n) {
    return n == svs::Dynamic ? std::string("dynamic") : fmt::format("{}", n);
}

//! [specialization-1]
// A specialized method.
// SVS defines the dispatch conversion from `svs::DataType` to `svs::lib::Type`.
// This overload takes an additional `std::string` argument type.
template <typename A, typename B, size_t N>
void specialized(
    svs::lib::Type<A> a_type,
    svs::lib::Type<B> b_type,
    svs::lib::ExtentTag<N> SVS_UNUSED(extent_tag),
    const std::string& arg
) {
    // Convert `svs::lib::Type` to `svs::DataType`.
    svs::DataType a = a_type;
    svs::DataType b = b_type;
    fmt::print(
        "Specialized with string: {}, {}, {} with arg \"{}\"\n", a, b, format_extent(N), arg
    );
}
//! [specialization-1]

//! [specialization-2]
template <typename A, typename B, size_t N>
void specialized_alternative(
    svs::lib::Type<A> a_type,
    svs::lib::Type<B> b_type,
    svs::lib::ExtentTag<N> SVS_UNUSED(extent_tag),
    bool flag
) {
    // Convert `svs::lib::Type` to `svs::DataType`.
    svs::DataType a = a_type;
    svs::DataType b = b_type;
    fmt::print(
        "Specialized with flag: {}, {}, {} with arg \"{}\"\n", a, b, format_extent(N), flag
    );
}
//! [specialization-2]

//! [generic-fallback]
struct Converted {
    std::string value_;
};
void generic(
    svs::DataType a_type,
    svs::DataType b_type,
    svs::lib::ExtentTag<svs::Dynamic> SVS_UNUSED(extent_tag),
    Converted converted
) {
    fmt::print(
        "Generic: {}, {}, {} with arg \"{}\"\n",
        a_type,
        b_type,
        format_extent(svs::Dynamic),
        converted.value_
    );
}
//! [generic-fallback]

} // namespace

//! [converted-dispatch-conversion-rules]
// Define full-specializations for converting `std::string` and `bool` to `Converted`.
template <> struct svs::lib::DispatchConverter<std::string, Converted> {
    // Return ``true`` if this is a match. Otherwise, return false.
    //
    // To provide finer-grained control, an ``int64_t`` can be returned instead, where
    // negative values indicate an invalid match (a method that has an invalid match for
    // any argument must be discarded), and positive values indicate degree of matches
    // with lower numbers having higher priority.
    static bool match(const std::string& SVS_UNUSED(arg)) { return true; }

    // This argument is called when a method has been selected and we're ready to convert
    // the source argument to the destination argument and invoke the registered target.
    static Converted convert(const std::string& arg) { return Converted{arg}; }

    // Provide documentation regarding the values accepted by this conversion.
    static std::string_view description() { return "all-string-values"; }
};

template <> struct svs::lib::DispatchConverter<bool, Converted> {
    static bool match(bool SVS_UNUSED(arg)) { return true; }
    static Converted convert(bool arg) { return Converted{fmt::format("boolean {}", arg)}; }
    static std::string_view description() { return "all-boolean-values"; }
};
//! [converted-dispatch-conversion-rules]

namespace {

//! [dispatcher-definition]
// A variant of types given for the last argument of the methods we wish to dispatch to.
using Variant = std::variant<bool, std::string>;

// A dispatcher wrapping and dispatching to functions that return void and whose arguments
// are constructible using dispatcher conversion from the remaining types.
using Dispatcher =
    svs::lib::Dispatcher<void, svs::DataType, svs::DataType, svs::lib::ExtentArg, Variant>;
//! [dispatcher-definition]

//! [register-methods]
Dispatcher build_and_register() {
    // Default construction for a dispatcher.
    auto dispatcher = Dispatcher{};

    // When registering methods, argument documentation can be requested using this tag.
    // Note dispatch rules are not required to implement documentation, in which case
    // default documentation will be provided.
    constexpr auto build_docs = svs::lib::dispatcher_build_docs;

    // Register the desired specializations.
    dispatcher.register_target(build_docs, &specialized<float, float, svs::Dynamic>);
    dispatcher
        .register_target(build_docs, &specialized_alternative<float, float, svs::Dynamic>);
    dispatcher.register_target(build_docs, &specialized<uint32_t, uint8_t, 128>);

    // Register the dynamic fallback.
    dispatcher.register_target(build_docs, &generic);
    return dispatcher;
}

const Dispatcher& get_dispatcher() {
    // Only allocated and populate the dispatcher once.
    static Dispatcher dispatcher = build_and_register();
    return dispatcher;
}
//! [register-methods]

} // namespace

//! [print-help]
void print_help() {
    constexpr std::string_view help_template = R"(
Usage:
    (1) dispatcher type_a type_b dims enforce_dims arg
    (2) dispatcher --help

1. Run the dispatcher example.
   * type_a and type_b: must be parseable as s `svs::DataType`.
   * dims: The number of dimensions to dispatch on. Can either be an integer or the string
     "dynamic"
   * enforce_dims: Whether or not relaxation to dynamic dimensionality is allowed. Must
     either be "true" or "false"
   * arg: An additional string argument. If arg is either "true" or "false", it will be
     parsed as a boolean. Otherwise, it will remain as a string and be forwarded to the
     appropriate overload.

2. Print this help message.

Registered Specializations
--------------------------
{{ type A, type B, Extent, Last Argument }}

{}
)";
    const auto& dispatcher = get_dispatcher();
    auto method_docs = std::vector<std::string>();
    constexpr size_t nargs = Dispatcher::num_args();
    for (size_t i = 0, imax = dispatcher.size(); i < imax; ++i) {
        auto arg_docs = std::vector<std::string>();
        for (size_t j = 0; j < nargs; ++j) {
            arg_docs.push_back(dispatcher.description(i, j));
        }
        method_docs.push_back(fmt::format("{{ {} }}", fmt::join(arg_docs, ", ")));
    }

    fmt::print(help_template, fmt::join(method_docs, "\n"));
}
//! [print-help]

//! [main]
int svs_main(const std::vector<std::string>& args) {
    // Perform some very basic
    const size_t nargs = args.size();
    bool requested_help = std::any_of(args.begin(), args.end(), [](const auto& arg) {
        return arg == "--help" || arg == "help";
    });
    if (nargs != 6 || requested_help) {
        print_help();
        return 0;
    }

    // Parse the argument types.
    svs::DataType type_a = parse_datatype(args.at(1));
    svs::DataType type_b = parse_datatype(args.at(2));
    svs::lib::ExtentArg extent_arg = parse_extent_arg(args.at(3), parse_bool(args.at(4)));

    // Construct a variant according to the value of `arg`.
    const auto& arg = args.at(5);
    auto maybe_bool = parse_bool_nothrow(arg);
    auto variant = [&]() -> Variant {
        if (maybe_bool) {
            return maybe_bool.value();
        }
        return arg;
    }();

    // Instantiate the dispatcher and dispatch to the best fitting method.
    get_dispatcher().invoke(type_a, type_b, extent_arg, variant);
    return 0;
}
//! [main]

// Main helper.
SVS_DEFINE_MAIN();