A couple ideas come to mind, but the key thing that I'm seeing is that you want to be able to take an arbitrary void function and call it with a single string. Templates can be really helpful here because you can use them to auto-deduce things such as how to build the tuple that you apply to the function.
This will be a semi-complicated meta-program-y solution, but I love that stuff; so I'm going to build a prototype. Also beware, this is the kind of solution that will result in absolutely horrendous compiler errors if you try to use it wrong.
My suggestion would be to make Command a templated type, where the command itself is templated on the parameter types of the function you want to pass it. If you need to be able to make a list of these to apply arguments to, then you can have a base class which provides the apply function. Since I don't fully understand how the argument specification is supposed to work, I'm punting on that and supporting keyword arguments only; but the way I built this, it should be fairly straightfoward to sub in your own argument splitter. I think. It could be cleaner, but I need to get back to my job.
Play with it on Compiler Explorer: https://godbolt.org/z/qqrn9bs1T
#include <any>
#include <functional>
#include <initializer_list>
#include <iostream>
#include <iterator>
#include <memory>
#include <regex>
#include <sstream>
#include <string>
#include <tuple>
#include <vector>
using namespace std;
// Converts the string arguments to the actual types
template <class T> T convert_arg(std::string);
template <> std::string convert_arg<std::string>(std::string s) { return s; }
template <> int convert_arg<int>(std::string s) { return std::stoi(s); }
// Split on spaces
std::vector<string> tokenize(std::string s) {
istringstream iss(s);
return {istream_iterator<string>{iss}, istream_iterator<string>{}};
}
// Argument spec defines how to parse the arguments from the input. It
// contains the positional index in the string, the name of it, and a
// default value. It's effectively a mapping from the string being applied
// to the function being called.
//
// This could maybe be turned into a std::tuple<std::tuple<...>>, but
// I'm not sure. That could get a little messy with trying to iterate
// through it to build the argument list, and I don't think it buys us
// anything.
//
// For example, given the argument spec
// {{1, "first_arg", 0}, {0, "second_arg", "some_default"}}
// You could call a function that has the signature
// void (int, string);
// And you could parse the following argument strings (assuming space-delimited)
// "second_arg=hello first_arg=0"
// "words 1"
// "first_arg=5 more_text"
using argument_spec_t = std::vector<tuple<std::size_t, string, std::string>>;
class CommandBase {
public:
virtual void apply(string args) = 0;
};
// Concrete commands are templated on the argument types of the function
// that they will invoke. For best results, use make_command() to deduce
// this template from the function that you want to pass the Command in
// order to get references and forwarding correct.
template <class... ArgTs> class Command : public CommandBase {
public:
using callable_t = std::function<void(ArgTs...)>;
// Holds the argument specification given during constuction; this
// indicates how to parse the string arguments
argument_spec_t m_argument_specification;
// A function which can be invoked
callable_t m_callable;
Command(argument_spec_t argument_spec, callable_t callable)
: m_argument_specification(std::move(argument_spec)),
m_callable(std::move(callable)) {}
void apply(string args) {
//std::cout << "Apply " << args << std::endl;
std::tuple parsed_args =
build_args(split_args(std::move(args), m_argument_specification),
std::index_sequence_for<ArgTs...>{});
std::apply(m_callable, parsed_args);
}
private:
// Pre-processes the command arguments string into a
// std::unordered_map<size_t, std::string> where x[i] returns the text of the
// i'th argument to be passed to the function.
//
// \todo Support positional arguments
// \todo Be more robust
static std::unordered_map<size_t, std::string>
split_args(std::string args, const argument_spec_t &arg_spec) {
std::unordered_map<std::string, std::string> kw_args;
std::unordered_map<size_t, std::string> arg_map;
vector<string> tokens = tokenize(args);
for (const auto &token : tokens) {
auto delim = token.find("=");
auto key = token.substr(0, delim);
auto val = token.substr(delim + 1);
kw_args[key] = val;
// std::cout << "key = " << val << std::endl;
}
for (size_t i = 0; i < arg_spec.size(); ++i) {
const auto &[pos_index, key, default_val] = arg_spec[i];
auto given_arg_it = kw_args.find(key);
if (given_arg_it != kw_args.end())
arg_map[i] = given_arg_it->second;
else
arg_map[i] = default_val;
// std::cout << i << " -> " << arg_map[i] << std::endl;
}
return arg_map;
}
// Copies the arguments from the map returned by pre_process_args into a
// std::tuple which can be used with std::apply to call the internal function.
// This uses a faux fold operation because I'm not sure the right way to do a
// fold in more modern C++
// https://articles.emptycrate.com/2016/05/14/folds_in_cpp11_ish.html
template <std::size_t... Index>
std::tuple<ArgTs...>
build_args(std::unordered_map<size_t, std::string> arg_map,
std::index_sequence<Index...>) {
std::tuple<ArgTs...> args;
std::initializer_list<int> _{
(std::get<Index>(args) =
convert_arg<std::tuple_element_t<Index, std::tuple<ArgTs...>>>(
std::move(arg_map[Index])),
0)...};
return args;
}
};
// Factory function to make a command which calls a pointer-to-member
// function. It's important that the reference to the object stays in
// scope as long as the Command object returned!
template <class C, class... ArgTs>
std::unique_ptr<CommandBase> make_command(C &obj,
void (C::*member_function)(ArgTs...),
argument_spec_t argument_spec) {
return std::make_unique<Command<ArgTs...>>(
std::move(argument_spec), [&obj, member_function](ArgTs... args) {
(obj.*member_function)(std::forward<ArgTs>(args)...);
});
}
// Factory function to make a command which calls a std::function.
template <class... ArgTs>
std::unique_ptr<CommandBase>
make_command(std::function<void(ArgTs...)> callable,
argument_spec_t argument_spec) {
return std::make_unique<Command<ArgTs...>>(std::move(argument_spec),
std::move(callable));
}
// Factory function to make a command which calls a free function
template <class... ArgTs>
std::unique_ptr<CommandBase> make_command(void (*fn)(ArgTs...),
argument_spec_t argument_spec) {
return make_command(std::function<void(ArgTs...)>{fn},
std::move(argument_spec));
}
class Action {
public:
void print_two_arguments(int arg1, int arg2) {
std::cout << arg1 << ", " << arg2 << std::endl;
}
void print_one_argument(std::string arg1) { std::cout << arg1 << std::endl; }
};
void print_one_argument_free(std::string arg1) {
std::cout << arg1 << std::endl;
}
int main() {
Action actor;
// my argument specification code splits by string and then extracts
// arguments by position or keyword and replacing with a default if
// not specified
auto command1 = make_command(actor, &Action::print_one_argument,
argument_spec_t{{0, "first_arg", "something"}});
command1->apply("first_arg=hello_world"); // Should print "hello_world"
auto command2 = make_command(
actor, &Action::print_two_arguments,
argument_spec_t{{0, "first_arg", "2"}, {1, "second_arg", "10"}});
command2->apply("0 second_arg=2"); // should print "0 2"*/
auto command3 = make_command(&print_one_argument_free,
argument_spec_t{{0, "first_arg", "something"}});
command3->apply("first_arg=hello_again");
}