如何将具有未知原型的成员函数传递给 C++ 中的 class?

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

我需要创建一个 class(我们称它为 Command),它接收一个字符串,将其处理为函数参数,然后将其传递给另一个函数的成员函数class。对于我的使用,我传递给 Command 的成员函数可能来自多个 classes,并且可能有许多不同的原型。我可以保证那个成员函数会 return void。这是我想象的代码:

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

我真的不介意那里有什么方法 - 我试过 std::bind 但不能完全让它工作,我也试过 lambdas。我目前正在尝试使用类型推导工厂方法的模板 class。我也对将在编译时修复此问题的宏定义持开放态度。

我认为有很多方法可以解决这个问题,包括带有可变参数的函数指针等。但是你的根本问题是你要求一个 class 理解另一个 class,这永远不会奏效。相反,我会争辩说你应该有一个父 Actor class 有一个可以被 sub-classes 覆盖的函数并且只传递一个 subclass 的实例] 反而。每个 subclass 可能需要接受一个参数数组,或者甚至每个 subclass 知道它从内部需要什么的另一种容器类型。

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

您实际应该做的事情需要分析您的目标是关于基指针和容器以及您的数据结构、流等。

我想到了几个想法,但我看到的关键是您希望能够采用任意 void 函数并使用单个字符串调用它。模板在这里非常有用,因为您可以使用它们来自动推断诸如如何构建应用于函数的元组等内容。

这将是一个半复杂的元程序 Y 解决方案,但我喜欢那个东西;所以我要建立一个原型。另请注意,如果您尝试错误地使用它,这种解决方案将导致绝对可怕的编译器错误。

我的建议是使 Command 成为模板化类型,其中命令本身根据您要传递给它的函数的参数类型进行模板化。如果你需要能够列出这些以应用参数,那么你可以有一个基础 class 提供 apply 功能。由于我不完全理解参数规范应该如何工作,所以我对此表示怀疑并仅支持关键字参数;但是我构建它的方式,在你自己的参数分离器中 sub 应该是相当直接的。我认为。它可能更干净,但我需要回到我的工作中。

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

在您的 apply 中,您描述了真正需要构造函数上下文的内容。如果 Command

会怎样
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);
  }
};

您仍然可以使用参数规范代码来创建 argument_spec 参数