C++ Command line action abstraction using interfac

2019-08-28 21:06发布

问题:

I'm building an application whose usage is going to look something like this:

application --command --option1=? --option2=2?

Basically, there can be any number of options, but only one command per instance of the application. Similar to the way git works.

Now, I thought I'd write it in C++ to get some boost and stl experience and have a go with a few of those design patterns I keep reading about. So, I implemented this:

class Action
{
public:
    void AddParameter(std::string key, boost::any p);
    virtual unsigned int ExecuteAction();

protected:
    std::map<std::string, boost::any> parameters;
};

I'll explain my logic anyway, just to check it - this is an abstract-ish action. All actions need option adding, hence the parameters map, so that we can implement at this level, but we expect ExecuteAction to be implemented by derived classes, such as my simple example DisplayHelpAction, which does pretty much what it says on the tin.

So now I've written a factory, like so:

class DetermineAction
{
public:
    DetermineAction();
    vx::modero::Action getAction(std::string ActionString);
private:
    std::map<std::string, vx::modero::Action> cmdmap;
};

The logic being that the constructor will create a map of possible strings you can ask for and getAction will do what it says - give it a command string and it'll give you a class that is derived from Action which implements the desired functionality.

I'm having trouble with that constructor. I am trying this:

this->cmdmap = std::map<std::string, Action>();
this->cmdmap.insert(pair<string, Action>("help", DisplayHelpAction()));
this->cmdmap.insert(pair<string, Action>("license", DisplayLicenseAction()));

Which is causing a lot of errors. Now, I'm used to the Java Way of interfaces, so you use:

Interface I = new ConcreteClass();

and Java likes it. So that's the sort of idea I'm trying to achieve here, because what I want do have for the implementation of getAction is this:

return this->cmdmap[ActionString];

Which should return a class derived from Action, on which I can then start adding parameters and call execute.

So, to summarise, I have two questions which are closely related:

  • Soundboard. I'm deliberately practising abstracting things, so there's some additional complexity there, but in principle, is my approach sound? Is there an insanely obvious shortcut I've missed? Is there a better method I should be using?
  • How can I set up my class mapping solution so that I can return the correct class? The specific complaint is link-time and is:

    Linking CXX executable myapp
    CMakeFiles/myapp.dir/abstractcmd.cpp.o: In function `nf::Action::Action()':
    abstractcmd.cpp:(.text._ZN2vx6modero6ActionC2Ev[_ZN2vx6modero6ActionC5Ev]+0x13): undefined reference to `vtable for nf::Action'
    

Just because it might be relevant, I'm using boost::program_options for command line parsing.


Edit 1: Ok, I have now replaced Action with Action* as per Eugen's answer and am trying to add new SomethingThatSubclassesAction to the map. I'm still getting the vtable error.

回答1:

  1. One thing than needs to be said right off the bat is that runtime polymorphism works in C++ via pointers to the base class not by value. So your std::map<std::string, Action> needs to be std::map<std::string, Action*> or your derived Actions (i.e. DisplayHelpAction) will be sliced when copied into the map. Storing Action* also mean that you'll need to explicitly take care of freeing the map values when you're done. Note: you can use a boost::ptr_map (boost::ptr_map<std::string,Action>) (as @Fred Nurk pointed out) or a boost::shared_ptr (std::map<std::string,boost::shared_ptr<Action> >) to not worry about explicitly freeing the Action* allocated.
    The same thing about 'Action getAction(std::string ActionString);' it needs to become Action* getAction(std::string ActionString);.

  2. The linker error is (most likely) caused by not providing an implementation for virtual unsigned int ExecuteAction();. Also I'd say it makes sense to make it pure virtual (virtual unsigned int ExecuteAction() = 0;) - in which case you don't need to provide an implementation for it. It will also provide the closes semantics to a Java interface for the Action class.

  3. Unless you have a very good reason for the Action derived objects to not know the entire boost:program_options I'd pass it down and let each of them access it directly instead of constructing std::map<std::string, boost::any>.

  4. I'd rename DetermineAction to something like ActionManager or ActionHandler.