C++ Function pointers with unknown number of argum

2019-04-06 02:14发布

问题:

I need some help with C++, please!

I'm writing a command parser for a small text-based game, and I've run into some problems. The parser is supposed to read and parse commands entered by the player.

The most obvious and straightforward solution to this could be something like this (written in pseudo-code):

command <- read input from the player
if command == COMMAND1
    do command1
else if command == COMMAND 2
    do command2
...

I'm writing in C++, so I was thinking I could solve this by using an associative map and function pointers. I'm not that familiar with using functions pointers, so that may be why I'm having problems. What I want to do, is to have some sort of loop that waits for input, parse the input that is inserted, and calls a function depending on the command given. Here's some C++-ish pseudo-code describing what I am thinking:

while(1) {
 cin >> input;
 char * tok = strtok(input, " ")
 functionpointer fptr = command_map.find(tok);
 ... // here, I get stuck on what to do..
}

So I hope I make myself somewhat clear on what I want to happen. The player could have had input something like

> go south

and I could have finished the code with something like:

destination = strtok(NULL, " ");
fptr(destination);

Basically, the value returned from the map would be the function that performs the command "go", and that function apparently takes one argument, the destination. Again, this is some C++-pseudo-ish code. So I got the command "go" covered. But now say that I want to have the follwing command:

> attack troll with sword

Now I feel that I need to do something like:

while(1) {
 cin >> input;
 char * tok = strtok(input, " ")
 functionpointer fptr = command_map.find(tok);
 if(tok == "go"){
    destination = strtok(NULL, " ");
    fptr(destination);
 } else if (tok == "attack") {
    target = strtok(NULL, " ");
    weapon = strtok(NULL, " ");
    fptr(target, weapon);
   }
}

Again, this is pseudo-code. You probably see what I get hung up on: I have this map of functions pointers, but because I have variable number of arguments and type of arguments because I want to call different functions depending on what I got as the input, so I could've just done this without a map and function pointers like I showed you first. Is there some way I can make this more general, without having to have some if-else clause to figure out how many arguments to pass?

I hope you understand what I need help with :) Thank you for reading!

回答1:

A better solution would be to have all of your functions take the same arguments. A good idea would be to first completely tokenize your input (say, into a vector of strings), and then pass that array to the functions. You could then use an associative container (such as a hash table or a std::map) to map command tokens to handler functions.

For example:

typedef std::vector<std::string> TokenArray;
typedef void (*CommandHandler)(const TokenArray&);
typedef std::map<std::string, CommandHandler> CommandMap;
void OnGo(const TokenArray& tokens)
{
    // handle "go" command
}
void OnAttack(const TokenArray& tokens)
{
    // handle "attack" command
}
// etc.

CommandMap handlers;
handlers["go"] = &OnGo;
handlers["attack"] = &OnAttack;
// etc.

while(1) {
  std::string input;
  cin >> input;
  std::istringstream tokenizer(input);
  TokenArray tokens;
  std::string token;
  while(tokenizer >> token)  // Tokenize input into whitespace-separated tokens
    tokens.push_back(token);
  CommandMap::iterator handler = handlers.find(tokens[0]);
  if(handler != handlers.end())
      (*handler)(tokens);  // call the handler
  else
      ; // handle unknown command
}


回答2:

Instead of having your main loop reading all the arguments needed for a 'passive' function, you can change your design to follow the Command design pattern, and have your function/command object do the argument parsing. That way you avoid needing to know the function's signature upfront.

You can use the Chain of Responsibility to find the proper Command, and let the Command consume the next tokens.

An example, using streams instead of strtok (heck we're C++ here, right?) - warning: uncompiled, untested, C++ish pseudo-code:

struct ICommand {
   // if cmd matches this command,
   // read all needed tokens from stream and execute
   virtual bool exec( string cmd, istream& s ) = 0;
};

struct Command : public ICommand {
   string name;
   Command( string name ):name(name){}
   virtual bool exec( string cmd, istream& s ) {
      if( cmd != name ) return false;
      parse_and_exec( s );
      return true;
   }
   virtual void parse_and_exec( istream& s ) = 0;
};

An implemented command:

struct Go : public Command {
   Go():Command("Go"){}

   virtual void parse_and_exec( istream& s ) {
        string direction;
        s >> direction;
        ... stuff with direction
   }
 };

And some main loop:

 ICommand* commands [] = 
 { new Go()
 , new Attack()
 , new PickUp()
 ...
 , NULL // a sentinel
 };

 string cmd;
 cin >> cmd;
 while( cmd != "quit" ) {
    // find a command that can handle it
    // note: too simple to handle erroneous input, end of stream, ...
    for( ICommand* c = commands; c != NULL && !c->exec(cmd, cin); ++c );
    cin >> cmd;
 }

You can refine this idea with stronger utility functions etc...

If you expect a really hard grammar, it may be better to step over to a 'real' parser framework, like e.g. boost::spirit.



回答3:

Have you thought about going a little OO. Have an abstract class say "Command" and have specialized classes for the specific commands.

struct Command
{
  virtual void Execute( const string &commandline ) = 0;
};

struct GoCommand: Command
{
  void Execute( const string &commandline )
  {
    // Do whatever you need here.
  }
}

Have a factory create the command objects based on the command entered by the user.

struct CommandFactory
{
  Command *GetCommand( const string &commandName )
  {
    if( commandNome == "go" )
    {
     return new GoCommand();
    }
    .........
    ........
  }
}

In the client get the command object and call the "Execute()" method

cin >> input;
char * commandName = strtok(input, " ")
Command *command = CommandFactory::Instance().GetCommand( commandName );
char * params = strtok(NULL, " ");
command->Execute( params );
delete command;

You can use auto pointer for better memory management.



回答4:

Only function parameter you need is the rest of the command line. Each function should tokenize it accordingly to its needs