How can I pass a member function with an unknown prototype to a class in C++?

Viewed 126

I need to make a class (we'll call it Command) that takes in a string, processes it into function arguments, and then passes it to a member function of a different class. For my use, the member function that I pass to Command could come from a number of classes, and could have many different prototypes. I can guarantee that that member function will return void. Here's the code I imagine:

class Command {
 public:
  vector<tuple<int, string, any>> argument_specification;
  SomeType callable;
  Command(vector<tuple<int, string, any>> argument_spec, SomeType callable) {
    this->argument_specification = argument_spec;
    this->callable = callable;
  }
  void apply(string args) {
    /* processing args according to this->argument_specification 
       to make a std::tuple arguments */
    std::apply(this->callable, arguments);
  }

};

class Action {
 public:
  print_two_arguments(int arg1, int arg2) {
    std::cout << arg1 << ", " << arg2 << std::endl;
  }
  print_one_arguments(std::string arg1) {
    std::cout << arg1 << std::endl);
  }
}

int main() {

Action *actor = new Action();

// my argument specification code splits by string and then extracts
// arguments by position or keyword and replacing with a default if
// not specified
Command *command1 = new Command({{0, "first_arg", "something"}},
                                &actor->print_one_argument);
command1->apply("hello_world"); // Should print "hello_world"

Command *command2 = new Command({{0, "first_arg", 2},
                                 {1, "second_arg", 10}},
                                &actor->print_two_arguments);
command2->apply("0 2"); // should print "0 2"
}

I don't really mind what method gets there - I've tried std::bind and can't quite get that to work, I've also tried lambdas. I'm currently trying a template class with a type deduced factory method. I'm also open to a macro definition that will fix this at compile time.

3 Answers

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");
}

I think there are a number of ways to handle this problem, including function pointers with variable arguments, etc. But your fundamental problem is that you're asking one class to understand the internals of another class, which never works out well. I'd argue instead that you should have a parent Actor class that has a function that can be overridden by sub-classes and just passing an instance of the subclass instead. Each subclass may need to take an array of arguments, or even another container type that each subclass knows what it needs from within.

#include <iostream>

using namespace std;

class Data {
 public:
   std::string strdata;
   int intinfo1;
   int intinfo2;
};
   
class ActionBase {
 public:
   virtual void act(Data d) = 0;
};


class PrintIntinfos : public ActionBase {
 public:
   virtual void act(Data d) {
    std::cout << d.intinfo1 << ", " << d.intinfo2 << std::endl;
   }
};


class PrintStrData : public ActionBase {
 public:
   virtual void act(Data d) {
    std::cout << d.strdata << std::endl;
   }
};


int main() 
{
    ActionBase *Action1 = new PrintIntinfos();
    Data d = Data();
    d.intinfo1 = 42;
    d.intinfo2 = -42;

    Action1->act(d);
    delete Action1;

    d.strdata = "hello world";
    
    Action1 = new PrintStrData();
    Action1->act(d);
}

What you should actually do requires analysis of what your goals are with respect to base-pointers and containers and your data structure, flow, etc.

In your apply you describe something that really wants the context of the constructor. What if Command was

class Command {
  std::function<void(std::string)> callable;
 public:
  template <typename... Args>
  Command(std::function<std::tuple<Args...>(std::string)> argument_spec, std::function<void(Args...)> callable)
    : callable([=](std::string args) { std::apply(callable, argument_spec(args)); })
  { }
  void apply(std::string args) {
    callable(args);
  }
};

You would still be able to use your argument specification code to create the argument_spec parameter

Related