【问题标题】:Call a C++ base class method automatically自动调用 C++ 基类方法
【发布时间】:2011-03-07 16:24:35
【问题描述】:

我正在尝试实现command design pattern,但我遇到了一个概念问题。假设您有一个基类和几个子类,如下例所示:

class Command : public boost::noncopyable {
    virtual ResultType operator()()=0;

    //Restores the model state as it was before command's execution.
    virtual void undo()=0;

    //Registers this command on the command stack.
    void register();
};


class SomeCommand : public Command {
    virtual ResultType operator()(); // Implementation doesn't really matter here
    virtual void undo(); // Same
};

问题是,每次在 SomeCommand 实例上调用运算符 () 时,我想通过调用 Command 的 register 方法将 *this 添加到堆栈中(主要用于撤消目的)。我想避免从 SomeCommand::operator()() 调用“注册”,而是自动调用它(不知何故 ;-))

我知道当你构造一个像 SomeCommand 这样的子类时,会自动调用基类构造函数,所以我可以在那里添加一个“注册”调用。在调用 operator()() 之前我不想调用 register 的东西。

我该怎么做?我想我的设计有些缺陷,但我真的不知道如何做到这一点。

【问题讨论】:

  • Command 的成员应该是公开的吗?
  • 感谢您的帮助。是的,它们应该是公开的,我忘了把它放在代码中。每次调用 SomeCommand 实例 operator() 时,我都想将它添加到堆栈中。您可以将其视为一种“撤消堆栈”。在构造 SomeCommand 对象和调用 operator() 之间可能会出现一些延迟。因此,我无法在构造时将其添加到堆栈中,这可能导致程序尝试撤消尚未完成的操作。
  • register 是关键字,不能命名方法 register。

标签: c++ class methods base-class


【解决方案1】:

您似乎可以从 NVI(非虚拟接口)习语中受益。 command 对象的接口没有虚拟方法,但会调用私有扩展点:

class command {
public:
   void operator()() {
      do_command();
      add_to_undo_stack(this);
   }
   void undo();
private:
   virtual void do_command();
   virtual void do_undo();
};

这种方法有不同的优点,首先是您可以在基类中添加通用功能。其他优点是您的类的接口和扩展点的接口没有相互绑定,因此您可以在公共接口和虚拟扩展接口中提供不同的签名。搜索NVI,你会得到更多更好的解释。

附录:Herb Sutter 的原始 article,他在其中介绍了这个概念(但未命名)

【讨论】:

  • 简直太棒了!非常感谢你们;)
  • 这就是为什么纯虚方法永远不应该进入公共接口的原因:你总是需要添加一些东西(日志记录、验证、预处理或后处理)。
  • @Dinaiz:很棒?告诉萨特,我刚刚复制了:)(顺便说一句:stackoverflow.com/users/297582/herb-sutter
  • 知道何时使用模式和习语比知道它们重要得多,所以这是当之无愧的赞美。
  • 很高兴看到这种出色的模式得到了应有的认可!这是一开始看起来有点倒退的事情(比如将方法从类中提取到独立函数中),但是当你仔细考虑它时,它显然是一个巨大的设计胜利。
【解决方案2】:

将运算符拆分为两种不同的方法,例如execute 和 executeImpl (老实说,我不太喜欢 () 运算符)。使Command::execute非虚拟,Command::executeImpl纯虚拟,然后让Command::execute进行注册,然后调用它executeImpl,像这样:

class Command
   {
   public:
      ResultType execute()
         {
         ... // do registration
         return executeImpl();
         }
   protected:
      virtual ResultType executeImpl() = 0;
   };

class SomeCommand
   {
   protected:
      virtual ResultType executeImpl();
   };

【讨论】:

  • 又好又聪明。这是我的 +1。
  • 同意不喜欢operator()。如果您需要将类传递给需要函数的东西,您可以随时使用bind 删除方法名称。
  • 你们不喜欢 () 运算符的原因是什么?
  • 你在这里所做的并不是异常安全的!如果 executeImpl() 抛出你的撤销堆栈中有一个从未(完全)执行的命令。在上面大卫罗德里格斯的回答中,它做得对。
  • @j_random_hacker:我发现 operator() 的另一个问题。一些(或大多数?)IDE 或 IDE 插件无法正确处理 operator()。例如。您无法在 Visual Assist X 中对 operator() 方法执行“查找所有引用”。但是,我不知道这是 Visual Assist X 中特有的问题,还是所有开发环境中的普遍问题。
【解决方案3】:

假设它是一个带有撤消和重做的“普通”应用程序,我不会尝试将管理堆栈与堆栈上的元素执行的操作混为一谈。如果您有多个撤消链(例如打开多个选项卡),或者当您执行撤消重做时,命令必须知道是将自身添加到撤消还是将自身从重做移动到撤消,这将变得非常复杂,或将自身从撤消移动到重做。这也意味着您需要模拟撤消/重做堆栈来测试命令。

如果您确实想混合使用它们,那么您将拥有三个模板方法,每个方法采用两个堆栈(或者命令对象需要在创建时引用它所操作的堆栈),并且每个执行移动或添加,然后调用该函数。但是如果你确实有这三种方法,你会发现它们实际上除了在命令上调用公共函数之外没有做任何事情,并且没有被命令的任何其他部分使用,所以下次重构代码时成为候选者为了凝聚力。

相反,我会创建一个具有 execute_command(Command*command) 函数的 UndoRedoStack 类,并让命令尽可能简单。

【讨论】:

    【解决方案4】:

    基本上,Patrick 的建议与 David 的建议相同,我的建议也相同。为此,请使用 NVI(非虚拟接口惯用语)。纯虚拟接口缺乏任何类型的集中控制。您也可以创建一个所有命令都继承的单独抽象基类,但何必呢?

    有关为什么需要 NVI 的详细讨论,请参阅 Herb Sutter 的 C++ 编码标准。在那里,他甚至建议将所有公共功能设为非虚拟,以实现可覆盖代码与公共接口代码的严格分离(这不应该是可覆盖的,以便您始终可以进行一些集中控制并添加仪器,pre/post-条件检查,以及您需要的任何其他内容)。

    class Command 
    {
    public:
       void operator()() 
       {
          do_command();
          add_to_undo_stack(this);
       }
    
       void undo()
       {
          // This might seem pointless now to just call do_undo but 
          // it could become beneficial later if you want to do some
          // error-checking, for instance, without having to do it
          // in every single command subclass's undo implementation.
          do_undo();
       }
    
    private:
       virtual void do_command() = 0;
       virtual void do_undo() = 0;
    };
    

    如果我们退后一步,看看一般问题,而不是直接提出问题,我认为 Pete 提供了一些非常好的建议。让 Command 负责将自己添加到撤消堆栈中并不是特别灵活。它可以独立于它所在的容器。这些更高级别的职责可能应该是实际容器的一部分,您也可以让它们负责执行和撤消命令。

    不过,学习 NVI 应该很有帮助。我见过太多的开发人员编写像这样的纯虚拟接口,因为他们只需要在定义它的每个子类中添加相同的代码,而只需要在一个中心位置实现它。这是一个非常方便的工具,可以添加到您的编程工具箱中。

    【讨论】:

      【解决方案5】:

      我曾经有一个项目来创建一个 3D 建模应用程序,并且我曾经有相同的要求。据我在处理它时所了解的,无论如何,操作都应该始终知道它做了什么,因此应该知道如何撤消它。所以我为每个操作创建了一个基类,它的操作状态如下所示。

      class OperationState
      {
      protected:
          Operation& mParent;
          OperationState(Operation& parent);
      public:
          virtual ~OperationState();
          Operation& getParent();
      };
      
      class Operation
      {
      private:
          const std::string mName;
      public:
          Operation(const std::string& name);
          virtual ~Operation();
      
          const std::string& getName() const{return mName;}
      
          virtual OperationState* operator ()() = 0;
      
          virtual bool undo(OperationState* state) = 0;
          virtual bool redo(OperationState* state) = 0;
      };
      

      创建一个函数及其状态如下:

      class MoveState : public OperationState
      {
      public:
          struct ObjectPos
          {
              Object* object;
              Vector3 prevPosition;
          };
          MoveState(MoveOperation& parent):OperationState(parent){}
          typedef std::list<ObjectPos> PrevPositions;
          PrevPositions prevPositions;
      };
      
      class MoveOperation : public Operation
      {
      public:
          MoveOperation():Operation("Move"){}
          ~MoveOperation();
      
          // Implement the function and return the previous
          // previous states of the objects this function
          // changed.
          virtual OperationState* operator ()();
      
          // Implement the undo function
          virtual bool undo(OperationState* state);
          // Implement the redo function
          virtual bool redo(OperationState* state);
      };
      

      曾经有一个类叫做 OperationManager。这注册了不同的函数并在其中创建了它们的实例,例如:

      OperationManager& opMgr = OperationManager::GetInstance();
      opMgr.register<MoveOperation>();
      

      注册函数是这样的:

      template <typename T>
      void OperationManager::register()
      {
          T* op = new T();
          const std::string& op_name = op->getName();
          if(mOperations.count(op_name))
          {
              delete op;
          }else{
              mOperations[op_name] = op;
          }
      }
      

      每当要执行一个函数时,它都将基于当前选择的对象或它需要处理的任何内容。注意:在我的例子中,我不需要发送每个对象应该移动多少的详细信息,因为一旦将其设置为活动功能,MoveOperation 就会从输入设备计算该移动量。
      在 OperationManager 中,执行一个函数就像这样:

      void OperationManager::execute(const std::string& operation_name)
      {
          if(mOperations.count(operation_name))
          {
              Operation& op = *mOperations[operation_name];
              OperationState* opState = op();
              if(opState)
              {
                  mUndoStack.push(opState);
              }
          }
      }
      

      当有必要撤消时,您可以从 OperationManager 执行此操作,例如:
      OperationManager::GetInstance().undo();
      而OperationManager的撤消功能是这样的:

      void OperationManager::undo()
      {
          if(!mUndoStack.empty())
          {
              OperationState* state = mUndoStack.pop();
              if(state->getParent().undo(state))
              {
                  mRedoStack.push(state);
              }else{
                  // Throw an exception or warn the user.
              }
          }
      }
      

      这使得 OperationManager 不知道每个函数需要什么参数,因此很容易管理不同的函数。

      【讨论】:

        猜你喜欢
        • 2016-10-05
        • 2015-03-30
        • 2011-10-30
        • 1970-01-01
        • 2017-11-29
        • 2019-08-04
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多