Search code examples
c++c++17interpreterstd-variant

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


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.


Solution

  • 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