I once had a project to create a 3D modelling application and for that I used to have the same requirement. As far as I understood when working on it was that no matter what and operation should always know what it did and therefore should know how to undo it. So I had a base class created for each operation and it's operation state as shown below.
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;
};
Creating a function and it's state would be like:
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);
};
There used to be a class called OperationManager. This registered different functions and created instances of them within it like:
OperationManager& opMgr = OperationManager::GetInstance();
opMgr.register<MoveOperation>();
The register function was like:
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;
}
}
Whenever a function was to be executed, it would be based on the currently selected objects or the whatever it needs to work on. NOTE: In my case, I didn't need to send the details of how much each object should move because that was being calculated by MoveOperation from the input device once it was set as the active function.
In the OperationManager, executing a function would be like:
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);
}
}
}
When there's a necessity to undo, you do that from the OperationManager like:
OperationManager::GetInstance().undo();
And the undo function of the OperationManager looks like this:
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.
}
}
}
This made the OperationManager not be aware of what arguments each function needs and so was easy to manage different functions.