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?
};
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.
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";
}
}