vendredi 23 décembre 2022

Overridable struct of params

I'm trying to find the most optimal way of doing the following. I have a big hierarchical structure of heterogeneous parameters like this (just as an example)

struct ServiceParams {
    struct FilterParams {
        bool remove_odds;
        bool remove_primes;
    }
    size_t length;
    std::optional<float> threshold;
    FilterParams filter_params;
}

The service which uses these parameters fills the values from a config file while starting, and then the service allows a user to provide their own parameters via request:


class Service {
    ServiceParams default_params;
    bool HandleRequest(Data data, std::optional<ServiceParams> user_params) {
         return DoSomeStuff(data, user_params.value_or(default_params))
    }
}

So the question is: what is the best way of allowing user to specify only some part of user_params and use other params stored in default_params.

What I did consider so far:

Dynamic structure like std::map<std::string, std::any>

I could use std::map<std::string, std::any> for parameters and just loop (recursively) through user provided key-value store and override all the parameters which were specified.

The main drawback is: hard to read and understand the code and static code analysers wouldn't show me the places where a certain parameter is used. And I need to provide getters for each of the parameters.

Make all the params to be std::optional
struct OptionalServiceParams {
    struct OptionalFilterParams {
        std::optional<bool> remove_odds;
        std::optional<bool> remove_primes;
    }
    std::optional<size_t> length;
    std::optional<std::optional<float>> threshold;
    std::optional<OptionalFilterParams> filter_params;
}

Drawback: but I don't want the inside implementations be bothered by these std::optional-s. It's not their job to extract contained values all the time.

Use both: the current and with std::optional-s

I can create the above optional structure for user's requests and user the current params structure for the inside implementation.

Drawback: I need to override all the parameters manually one by one

auto params = default_params;
if (user_params.length.has_value()) 
    params.length = user_params.length.value();
if (user_params.threshold.has_value()) 
    params.threshold = user_params.threshold.value();
if (user_params.filter_params.has_value()) {
    if (user_params.filter_params->remove_odds.has_value())
        params.filter_params.remove_odds = user_params.filter_params->remove_odds.value();
    if (user_params.filter_params->remove_primes.has_value())
        params.filter_params.remove_primes = user_params.filter_params->remove_primes.value();
}

It's easy to make a bug and it's hard to maintain and scale. And of course it's a lot of code duplication.

Make the above two structures as one but template

We could make it as (truncated, the question becomes too long)

template <template <typename ValueType>, typename...> typename Container = std::type_identity_t>
struct ServiceParams {
    Container<size_t> length;
    Container<std::optional<float>> threshold;
}

and use it like this

ServiceParams<> default_params;
ServiceParams<std::optional> user_params;

Now I can use default_params just as is (inside implementation) and there is no code duplication. Drawback: it doesn't solve the problem with manual overriding all the params. And in contrast with the previous approach it's almost impossible to support designated initialisers here.

Resume

The other languages (e.g. python) provides reflection and I can treat a structure as a key-value store without a need of making getters. Plus static code analysers shows me the places where a certain parameter is used.

I'm 100% sure some classic approach exists but I failed to find it online.

Aucun commentaire:

Enregistrer un commentaire