Назовите метод базового класса C++ автоматически

Я пытаюсь реализовать шаблон разработки команды, но я спотыкаюсь через концептуальную проблему. Скажем, у Вас есть базовый класс и несколько подклассов как в примере ниже:

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, я хотел бы добавить *это к стеку (главным образом в целях отмены) путем вызова метода регистра Команды. Я хотел бы постараться не называть "регистр" от SomeCommand:: оператор () (), но назвать его автоматически (некоторым образом ;-))

Я знаю, что, когда Вы создаете sub класс, такой как SomeCommand, конструктора базового класса вызывают автоматически, таким образом, я мог добавить вызов, чтобы "зарегистрироваться" там. Вещь, которую я не хочу называть регистром до оператора () () называют.

Как я могу сделать это? Я предполагаю, что мой дизайн несколько испорчен, но я действительно не знаю, как сделать эту работу.

11
задан Peter Mortensen 5 September 2013 в 19:50
поделиться

5 ответов

Похоже, что вы можете воспользоваться идиомой NVI (Non-Virtual Interface). В этом случае интерфейс объекта 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, и вы получите гораздо больше и лучше объяснений.

Дополнение: Оригинальная статья Херба Саттера, где он вводит концепцию (пока безымянную)

28
ответ дан 3 December 2019 в 02:00
поделиться

Разделите оператор на два разных метода, например, 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();
   };
6
ответ дан 3 December 2019 в 02:00
поделиться

В основном предложение Патрика совпадает с предложением Дэвида, которое также совпадает с моим. Используйте NVI (идиома невиртуального интерфейса) для этой цели. Чистые виртуальные интерфейсы лишены какого-либо централизованного контроля. В качестве альтернативы можно создать отдельный абстрактный базовый класс, от которого наследуют все команды, но зачем утруждать себя?

Подробное обсуждение того, почему NVI желательны, можно найти в "Стандартах кодирования C++" Херба Саттера. Там он доходит до того, что предлагает сделать все публичные функции невиртуальными, чтобы добиться строгого отделения переопределяемого кода от кода публичного интерфейса (который не должен быть переопределяемым, чтобы вы всегда могли иметь централизованный контроль и добавить инструментарий, проверку предварительных и последующих условий и все остальное, что вам нужно).

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

Если мы сделаем шаг назад и посмотрим на общую проблему, а не на конкретный заданный вопрос, я думаю, что Пит предлагает очень хороший совет. Заставлять Command отвечать за добавление себя в стек отмены не очень гибко. Она может быть независима от контейнера, в котором находится. Эти обязанности более высокого уровня, вероятно, должны быть частью фактического контейнера, который вы также можете сделать ответственным за выполнение и отмену команды.

Тем не менее, изучение NVI должно быть очень полезным. Я видел слишком много разработчиков, которые писали чистые виртуальные интерфейсы, подобные этому, из исторической выгоды, которую они имели, только для того, чтобы добавить тот же самый код в каждый подкласс, который его определяет, когда он должен быть реализован только в одном центральном месте. Это очень удобный инструмент, который можно добавить в свой набор инструментов программиста.

0
ответ дан 3 December 2019 в 02:00
поделиться

Однажды у меня был проект по созданию приложения для 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 не знал, какие аргументы нужны каждой функции, и поэтому было легко управлять различными функциями.

0
ответ дан 3 December 2019 в 02:00
поделиться

Предполагая, что это «нормальное» приложение с отменой и повтором, я бы не стал смешивать управление стеком с действиями, выполняемыми элементами в стеке. Это будет очень сложно, если у вас либо несколько цепочек отмены (например, открыто более одной вкладки), либо когда вы выполняете отмену-повтор, когда команда должна знать, добавлять ли себя для отмены или перемещаться от повторения к отмене, или перейти от отмены к повторению. Это также означает, что вам нужно имитировать стек отмены / повтора для проверки команд.

Если вы действительно хотите их смешать, у вас будет три метода шаблона, каждый из которых принимает два стека (или объект команды должен иметь ссылки на стеки, с которыми он работает при создании), и каждый выполняет перемещение или добавление , а затем вызов функции. Но если у вас есть эти три метода, вы увидите, что они на самом деле не делают ничего, кроме вызова общедоступных функций в команде и не используются никакими другими частями команды, поэтому станьте кандидатами в следующий раз, когда вы рефакторируете свой код. для сплоченности.

Вместо этого я бы создал класс UndoRedoStack, который имеет функцию execute_command (Command * command), и оставил бы команду как можно более простой.

1
ответ дан 3 December 2019 в 02:00
поделиться
Другие вопросы по тегам:

Похожие вопросы: