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

Я знаю, что когда вы создаете подкласс, такой как SomeCommand, конструктор базового класса вызывается автоматически, поэтому я мог бы добавить туда вызов для «регистрации». То, что я не хочу вызывать, регистрируется до тех пор, пока не будет вызван оператор()().

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


person Dinaiz    schedule 24.06.2010    source источник
comment
Члены группы Command должны быть публичными?   -  person CB Bailey    schedule 24.06.2010
comment
Спасибо за помощь. Да, они должны быть общедоступными, и я забыл указать это в коде. Каждый раз, когда вызывается оператор экземпляра SomeCommand(), я хотел бы добавить его в стек. Вы можете видеть это как своего рода стек отмены. Между моментом создания объекта SomeCommand и моментом вызова operator() может возникнуть некоторая задержка. Поэтому я не могу добавить его в стек при построении, потому что это может привести к тому, что программа попытается отменить что-то, что еще не было сделано.   -  person Dinaiz    schedule 24.06.2010
comment
register является ключевым словом, вы не можете назвать регистр метода.   -  person Alexandre C.    schedule 24.06.2010


Ответы (5)


Похоже, вы можете извлечь выгоду из идиомы 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, и вы получите гораздо больше и лучшие объяснения.

Приложение: Оригинальная статья Херба Саттера, в которой он представляет концепцию (пока без названия)

person David Rodríguez - dribeas    schedule 24.06.2010
comment
Просто блестяще! Большое спасибо ребята ;) - person Dinaiz; 24.06.2010
comment
Вот почему чистые виртуальные методы никогда не должны появляться в публичном интерфейсе: вам всегда нужно что-то добавлять (логирование, проверку, предварительную или постобработку). - person Matthieu M.; 24.06.2010
comment
@Dinaiz: Гениально? Скажи это Саттеру, я только что скопировал :) (Кстати: stackoverflow.com/users/297582/herb-sutter ) - person David Rodríguez - dribeas; 24.06.2010
comment
Знать, когда использовать шаблоны и идиомы, гораздо важнее, чем знать их, поэтому это заслуженная похвала. - person Gorpik; 24.06.2010
comment
Приятно видеть, что эта превосходная модель получает заслуженное признание! Это одна из тех вещей, которые поначалу кажутся отсталыми (например, вытягивание методов из класса в автономные функции), но если хорошенько об этом подумать, то становится ясно, что это большая победа в дизайне. - person j_random_hacker; 24.06.2010

Разделите оператор на два разных метода, например. 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();
   };
person Patrick    schedule 24.06.2010
comment
Красиво и умно. Вот мой +1. - person ereOn; 24.06.2010
comment
Согласен, не нравится operator(). Если вам нужно передать класс чему-то, что принимает функцию, вы всегда можете использовать bind для удаления имени метода. - person David Rodríguez - dribeas; 24.06.2010
comment
По какой причине вам, ребята, не нравится оператор ()? - person Dinaiz; 24.06.2010
comment
@Dinaiz: 1: становится более неясно, что должна делать функция (документация). 2: Большие изменения конфликтов имен (особенно с интерфейсами, которые имеют виртуальные () операторы. - person Patrick; 24.06.2010
comment
То, что вы здесь делаете, небезопасно! Если executeImpl() срабатывает, у вас есть команда в стеке отмены, которая никогда (полностью) не выполнялась. В ответе Дэвида Родригеса выше все сделано правильно. - person Fabio Fracassi; 24.06.2010
comment
Мне также нравится operator()() для метода main/default. @ Патрик, я не вижу, насколько лучше обстоят дела с функциями с регулярными именами - любой метод, который можно было бы разумно назвать operator()(), вероятно, был бы назван run() или execute(), если бы вы использовали обычные имена, поэтому вероятность конфликта имен не велика. гораздо ниже ИМХО. (Есть контрпример?) - person j_random_hacker; 24.06.2010
comment
@j_random_hacker: Однажды я видел пример, когда кто-то создавал интерфейсы наблюдателя, используя операторы скобок: DoneThisObserver с чисто виртуальным оператором скобок, DoneThatObserver с чисто виртуальным оператором скобок и так далее. В результате реализации наблюдателя должны были быть реализованы как разные классы и не могли быть реализованы в одном классе. Наконец кто-то сделал классы-адаптеры, которые сопоставляли оператор () с методами onDoneThis, onDoneThat, и тогда это заработало, но конечный результат был ужасен. - person Patrick; 24.06.2010
comment
@Patrick: Понимаешь, что ты имеешь в виду. Да, если вы заранее знаете, что несколько различных основных методов могут сосуществовать в одном и том же классе, присвоение им разных имен значительно упростит задачу. Но если вы не знаете этого заранее, т.е. если вы разрабатываете наблюдатель общего назначения в стиле сигналов и слотов Boost, то по определению вам нужно общее имя метода, и тогда operator()() почти так же хорош, как, скажем, execute(). - person j_random_hacker; 24.06.2010
comment
@j_random_hacker: я обнаружил дополнительную проблему с оператором(). Некоторые (или большинство?) IDE или плагины IDE не могут корректно обрабатывать оператор(). Например. вы не можете выполнить поиск всех ссылок в Visual Assist X для метода operator(). Однако я не знаю, проблема ли это конкретно в Visual Assist X или общая проблема во всех средах разработки. - person Patrick; 24.06.2010
comment
@Patrick: Я полностью согласен с этим :) Иногда кажется, что C ++ был разработан специально для того, чтобы максимально приблизиться к неразборчивости ... :) - person j_random_hacker; 24.06.2010

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

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

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

person Pete Kirkham    schedule 24.06.2010

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

Подробное обсуждение того, почему NVI желательны, см. в статье Herb Sutter Standards Coding Standards. Там он заходит так далеко, что предлагает сделать все публичные функции невиртуальными, чтобы добиться строгого отделения переопределяемого кода от общедоступного кода интерфейса (который не должен быть переопределяемым, чтобы вы всегда могли иметь некоторый централизованный контроль и добавлять инструментарий, до/после). проверка состояния и все, что вам нужно).

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

person stinky472    schedule 24.06.2010

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

person Vite Falcon    schedule 24.06.2010