Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Too many if/else statements in converting user inputs to types in C++

I have a template class with 3 template arguments.

template <class T, class U, class Y>
class MyClass {};

I wanna get input from users by CLI arguments, something like ./cli float driver-x load

  • The first arg can be float or double
  • The second arg is a driver name: driver-x, driver-y, ...
  • The third argument is about the action type: load, unload, ...

If I want to create a new instance of MyClass based on user inputs, I have to define many if/else statements. Because a user inputs are string and I have to prepare a condition on them. So, it will be something like this:

if (data_type == "float")
    if (driver == "driver-x")
        if (action == "load")
            MyClass<float, DriverX, Load> t;
            t......

As far as I know, it's impossible to store a type in a variable in C++.

So, is there any way exists to improve the if/else statements? Something like:

if (data_type == "float")
    //

if (driver == "driver-x")
   //

if (action == "load")
   //

MyClass<......> t;
t.....;

Or any other way?

I'm looking for a way to improve these if/else statements.

like image 414
mortymacs Avatar asked Dec 10 '22 00:12

mortymacs


2 Answers

Here's my take

template<typename T>
struct proxy { // or std::type_identity
    using type = T;
};
template<typename... Ts>
using choice_of = std::variant<proxy<Ts>...>;

template<typename T, typename>
using type_const_t = T;

template<typename T, typename... Ts>
std::optional<choice_of<T, Ts...>> choose(std::string const &choice, std::string const &head, type_const_t<std::string const&, Ts>... tail) noexcept {
    if(choice == head) return proxy<T>{};
    else if constexpr(sizeof...(Ts) == 0) return std::nullopt;
    else if(auto rec = choose<Ts...>(choice, tail...)) return std::visit(
        [](auto rec) -> choice_of<T, Ts...> { return rec; },
        *rec); 
    else return std::nullopt;
}

auto data_choice = choose<float, double>(data_type, "float", "double");
auto driver_choice = choose<DriverX, DriverY>(driver, "driver-x", "driver-y");
auto action_choice = choose<Load, Unload>(action, "load", "unload");
std::visit([](auto data_type_p, auto driver_p, auto action_p) {
    auto t = MyClass<typename decltype(data_type_p)::type, typename decltype(driver_p)::type, typename decltype(action_p)::type>{};
    // do stuff with t
}, data_choice.value(), driver_choice.value(), action_choice.value());

Complete example on Godbolt

like image 134
HTNW Avatar answered May 18 '23 01:05

HTNW


You can build some machinery to do this for you, extracting it into a function call.

For example, here I build a tuple which contains strings and types, then I check a passed string against all of them:

#include <string_view>
#include <cstddef>
#include <tuple>
#include <utility>
#include <type_traits>

template<class T>
struct mapped_type {
    const std::string_view key;
    using type = T;

    explicit constexpr operator bool() const noexcept {
        return true;
    }
};

namespace detail {
    template<class K, class F, class M, std::size_t I>
    constexpr void lookup_impl(const K& key, F&& f, M&& m, std::integral_constant<std::size_t, I>) {
        using tuple_t = typename std::remove_cv<typename std::remove_reference<M>::type>::type;
        if constexpr (I < std::tuple_size<tuple_t>::value) {
            const auto& mapping = std::get<I>(m);
            if (mapping.key == key) {
                std::forward<F>(f)(mapping);
                return;
            }
            lookup_impl(key, std::forward<F>(f), std::forward<M>(m), std::integral_constant<std::size_t, I + 1>{});

        } else {
            std::forward<F>(f)(std::false_type{});
        }
    }
}

// Calls `f` with the first value from `m` that matches the key
// or `std::false_type{}` if no key matches.
template<class K, class F, class M>
constexpr void lookup(const K& key, F&& f, M&& m) {
    detail::lookup_impl(key, std::forward<F>(f), std::forward<M>(m), std::integral_constant<std::size_t, 0>{});
}

// This is our mapping for the first argument
inline constexpr auto data_type_map = std::make_tuple(
    mapped_type<float>{ "float" },
    mapped_type<double>{ "double" }
);

// Example usage
#include <iostream>

int main() {
    const char* s = "float";

    lookup(s, [](const auto& arg) {
        if constexpr (!arg) {
            std::cout << "Invalid type\n";
        } else {
            using type = typename std::remove_cv<typename std::remove_reference<decltype(arg)>::type>::type::type;
            std::cout << "Got type: " << typeid(type).name() << '\n';
        }
    }, data_type_map);
}

And then you can call this recursively inside the lambda.

You could also create a version that takes a tuple of keys and a tuple of values to call one function with many arguments:

#include <string_view>
#include <tuple>
#include <utility>
#include <type_traits>

template<class T>
struct mapped_type {
    const std::string_view key;
    using type = T;

    explicit constexpr operator bool() const noexcept {
        return true;
    }
};

namespace detail {
    template<class K, class F, class M, std::size_t I>
    constexpr void lookup_impl(F&& f, const K& key, M&& m, std::integral_constant<std::size_t, I>) {
        using tuple_t = typename std::remove_cv<typename std::remove_reference<M>::type>::type;
        if constexpr (I < std::tuple_size<tuple_t>::value) {
            const auto& mapping = std::get<I>(m);
            if (mapping.key == key) {
                std::forward<F>(f)(mapping);
                return;
            }
            lookup_impl(std::forward<F>(f), key, std::forward<M>(m), std::integral_constant<std::size_t, I + 1>{});
        } else {
            std::forward<F>(f)(std::false_type{});
        }
    }

    template<class F, class K, class M, std::size_t I>
    constexpr void multilookup_impl(F&& f, const K& keys, M&& mappings, std::integral_constant<std::size_t, I>) {
        constexpr std::size_t size = std::tuple_size<typename std::remove_cv<typename std::remove_reference<K>::type>::type>::value;
        if constexpr (I >= size) {
            std::forward<F>(f)();
        } else {
            lookup_impl([&](const auto& current_lookup) {
                multilookup_impl(
                    [&](const auto&... args) { std::forward<F>(f)(current_lookup, args...); },
                    keys, mappings, std::integral_constant<std::size_t, I + 1>{}
                );
            }, std::get<I>(keys), std::get<I>(mappings), std::integral_constant<std::size_t, 0>{});
        }
    }
}

template<class F, class K, class M>
constexpr void lookup(F&& f, const K& keys, M&& mappings) {
    using map_tuple_t = typename std::remove_cv<typename std::remove_reference<M>::type>::type;
    using key_tuple_t = typename std::remove_cv<typename std::remove_reference<K>::type>::type;
    constexpr std::size_t size = std::tuple_size<key_tuple_t>::value;
    static_assert(size == std::tuple_size<map_tuple_t>::value, "Wrong number of keys for given number of maps");
    detail::multilookup_impl(std::forward<F>(f), keys, mappings, std::integral_constant<std::size_t, 0>{});
}

Which looks almost the same, but there's one more level of calls.

It would be used like this:

#include <iostream>


inline constexpr auto data_type_map = std::make_tuple(
    mapped_type<float>{ "float" },
    mapped_type<double>{ "double" }
);

inline constexpr auto driver_type_map = std::make_tuple(
    mapped_type<DriverX>{ "driver-x" },
    mapped_type<DriverY>{ "driver-y" }
);

inline constexpr auto action_type_map = std::make_tuple(
    mapped_type<Load>{ "load" },
    mapped_type<Unload>{ "unload" }
);

int main() {
    const char* a = "float";
    const char* b = "driver-x";
    const char* c = "load";

    lookup([](const auto& data, const auto& driver, const auto& action) {
        if constexpr (!data) {
            std::cout << "Could not parse data!\n";
        } else if constexpr (!driver) {
            std::cout << "Could not parse driver!\n";
        } else if constexpr (!action) {
            std::cout << "Could not parse action!\n";
        } else {
            using data_type = typename std::remove_cv<typename std::remove_reference<decltype(data)>::type>::type::type;
            using driver_type = typename std::remove_cv<typename std::remove_reference<decltype(driver)>::type>::type::type;
            using action_type = typename std::remove_cv<typename std::remove_reference<decltype(action)>::type>::type::type;

            MyClass<data_type, driver_type, action_type> t;
            std::cout << "Constructed a " << typeid(decltype(t)).name() << '\n';
        }
    },
        std::array<const char*, 3>{ a, b, c },
        std::forward_as_tuple(data_type_map, driver_type_map, action_type_map)
    );
}
like image 44
Artyer Avatar answered May 18 '23 00:05

Artyer