How to store parametric, strongly typed function for a text-based command console

96 views Asked by At

The use-case

I'm trying to build a small interpreter for a text-based command console. E.g., say there's a function somewhere:

void SetBrightness(float brightness) { /* ... */ }

Then it should be possible to write in the console "SetBrightness 0.5", which should result in the above function being called. We can register a command at runtime:

void AddCommand(const std::string& command, CommandInterpreterCallback* callback);

This function should bind not only a callback function, but also a list of strongly typed parameters matching to what the callback expects, to the string command. I define a class to hold the callback:

using ParamType = std::variant<int32_t, float, std::string, bool>;

class CommandInterpreterCallback
{
public:
    void SetParameters(const std::vector<ParamType>& args)
    {
        mNumParameters = args.size();
        if (mNumParameters >= 1) { mArg1 = args[0]; }
        // ...
    }

    void SetFunction(std::function<void> callback)
    {
        mCallback = callback; // TODO: How to solve this?
    }

private:
    uint8_t mNumParameters = 0;
    ParamType mArg1 = ParamType<bool>; // possibly an array instead here

    std::function<void> mCallback; // TODO: How to solve this?
};

The challenge

I really wish for the parameter passing to be strongly-typed, e.g. the function SetBrightness() must have a floating-point number. So if the user writes "SetBrightness true", the function should not get called. Also, I do not want to specify the function parameter as an std::variant<...>, because it looks messy and makes calling the function from other parts of the code more difficult.

But I'm not sure how I can declare the mCallback member inside the class, because it should somehow be parametric. I understand that I can set the type to e.g. std::function<void(float)>. But then, what if I want to bind another function which accepts a boolean?

Maybe I could use std::variant as well to specify the type of mCallback, but that sounds like an exponential complexity solution.

Is there a good way around these limitations? I hope I describe the problem well enough.

1

There are 1 answers

7
Jarod42 On BEST ANSWER

You might start with something like:

static std::map<std::string, std::function<void(const std::string&)>> commands;

template <typename T>
T extract(std::stringstream& ss)
{
    T t{};
    ss >> t;
    if (!ss) {
        throw std::runtime_error("Invalid argument");
    }
    return t;
}

template <typename...Ts>
void AddCommand(const std::string& name, void(*f)(Ts...))
{
    commands.emplace(name, [f](const std::string& s){
        std::stringstream ss(s);
        try {
            // {..} guarantees left-to-right order.
            std::tuple args = {extract<std::decay_t<Ts>>(ss)...}; 

            std::apply([f](const auto&... args){ f(args...); }, args);
        } catch (const std::exception& ex) {
            std::cout << "ERROR: " << ex.what() << std::endl;
        }
    });
}

void parseCommand(const std::string& s)
{
    std::stringstream ss(s);

    std::string command;
    ss >> command;
    if (auto it = commands.find(command); it != commands.end()) {
        std::string arg;
        std::getline(ss, arg);
        it->second(arg);
    } else {
        std::cout << "Unknown command\n";
    }
}

Demo