#include <cassert>
#include <vector>
#include <memory>
#include <experimental/any>
#include <experimental/optional>
#include <functional>
// for any, any_cast and optional
using namespace std::experimental;
template<class TransactionManagerType>
class TransactionManager;
template<class TransactionManagerType>
class Action {
public:
virtual ~Action() = default;
/**
* Performs the action on the given transaction manager.
* @param transactionManager The transaction manager to perform the transaction on.
*/
virtual void perform(TransactionManagerType *transactionManager) noexcept = 0;
/**
* Undos the action on the given transaction manager.
* @param transactionManager The transaction manager to undo the transaction on.
*/
virtual void undo(TransactionManagerType *transactionManager) noexcept = 0;
};
template<class TransactionManagerType, class ActionType>
class Transaction {
public:
explicit Transaction(TransactionManagerType *transactionManager) :
transactionManager(transactionManager), finalized(false) {}
virtual ~Transaction() = default;
const optional<ActionType> &getAction() const {
return actionOpt;
}
void setAction(ActionType action) {
assert(!finalized);
if (actionOpt) {
// undo the existing action, if any
actionOpt->undo(transactionManager);
}
// perform the new action
action.perform(transactionManager);
// set the existing action
actionOpt = action;
}
void finalize() {
assert(!finalized);
finalized = true;
transactionManager->transactionFinalized(
this,
// implicitly provide the ActionType to transactionFinalized
// using a dummy pointer, to avoid having to explicitly provide
// the transaction's type (which we don't have)
static_cast<ActionType *>(nullptr));
}
private:
TransactionManagerType *transactionManager;
bool finalized;
/**
* The current action, if any.
*/
optional<ActionType> actionOpt;
};
/**
* ActionPerformer is a utility class implicitly remembering
* the type of the action contained in the lambda functions
* it is created with.
* This is required to avoid having to add the action type
* as a template argument, which would make it impossible
* to store ActionPerformers for different action types
* on a TransactionManager's undo and redo stack.
*/
template<class TransactionManagerType>
class ActionPerformer {
public:
ActionPerformer(const std::shared_ptr<any> &action,
const std::function<void(TransactionManagerType *)> &undoAction,
const std::function<void(TransactionManagerType *)> &redoAction) :
action(action), undoAction(undoAction), redoAction(redoAction) {}
void undo(TransactionManager<TransactionManagerType> *manager) {
undoAction(static_cast<TransactionManagerType *>(manager));
}
void redo(TransactionManager<TransactionManagerType> *manager) {
redoAction(static_cast<TransactionManagerType *>(manager));
}
private:
/**
* The action to perform.
*/
std::shared_ptr<any> action;
std::function<void(TransactionManagerType *)> undoAction, redoAction;
};
template<class TransactionManagerType>
class TransactionManager {
public:
TransactionManager() = default;
virtual ~TransactionManager() = default;
template<class ActionType>
Transaction<TransactionManagerType, ActionType> *createTransaction() {
// there must not be a current unfinalized transaction
assert(currentTransaction.empty());
currentTransaction = Transaction<TransactionManagerType, ActionType>(
// cast from TransactionManager<TransactionManagerType> to TransactionManagerType.
static_cast<TransactionManagerType *>(this));
return any_cast<Transaction<TransactionManagerType, ActionType>>(¤tTransaction);
};
/**
* Undos the previous transaction.
*/
void undo() noexcept {
assert(!undoStack.empty());
if (undoStack.empty()) return;
auto &performer = undoStack.back();
// undo the action
performer.undo(this);
// add the action performer to the redo stack
redoStack.push_back(performer);
// remove the transaction from the undo stack
undoStack.pop_back();
}
/**
* Redos the next transaction.
*/
void redo() noexcept {
assert(!redoStack.empty());
if (redoStack.empty()) return;
auto &performer = redoStack.back();
// redo the action
performer.redo(this);
// add the action performer to the undo stack
undoStack.push_back(performer);
// remove the transaction from the redo stack
redoStack.pop_back();
}
/**
* Called by the current transaction when it is finalized.
* @param transaction The transaction that was finalized.
*/
template<class TransactionType, class ActionType>
void transactionFinalized(TransactionType *transaction, ActionType *dummy) {
auto &actionOpt = transaction->getAction();
if (actionOpt) {
// clear the redo stack
redoStack.clear();
// add the transaction's action to the undo stack
auto action = *actionOpt;
// create a shared pointer containing a copy of the action
// as an "any" instance to circumvent the need of a
// template argument for the Action Type in the
// ActionPerformer class, which makes it possible
// to store ActionPerformers of different ActionTypes
// in a vector. The ActionType is retained in the
// lambda function.
auto actPtr = std::make_shared<any>(ActionType(action));
undoStack.emplace_back(actPtr, [actPtr
](TransactionManagerType *manager) {
any_cast<ActionType>(actPtr.get())->undo(manager);
}, [actPtr](TransactionManagerType *manager) {
any_cast<ActionType>(actPtr.get())->perform(manager);
});
}
// clear the current transaction
currentTransaction.clear();
};
private:
/**
* The current unfinalized transaction, if any.
*/
any currentTransaction;
/**
* The stacks of actions to undo and redo.
*/
std::vector<ActionPerformer<TransactionManagerType>> undoStack, redoStack;
};
#define TRANSACTION_MANAGER(className) class className : public TransactionManager<className>
TRANSACTION_MANAGER(TestTransactionManager) {
public:
TestTransactionManager() : value(0) {}
int value;
};
class AdditionAction : public Action<TestTransactionManager> {
public:
explicit AdditionAction(int amount) : amount(amount) {}
void perform(TestTransactionManager *transactionManager) noexcept override {
transactionManager->value += amount;
}
void undo(TestTransactionManager *transactionManager) noexcept override {
transactionManager->value -= amount;
}
int amount;
};
int main(int argc, char *argv[]) {
TestTransactionManager manager;
{
auto *transaction = manager.createTransaction<AdditionAction>();
transaction->setAction(AdditionAction(5));
transaction->finalize();
assert(manager.value == 5);
}
{
// create an empty transaction - it should
// not affect the undo/redo behaviour
auto *transaction = manager.createTransaction<AdditionAction>();
transaction->finalize();
assert(manager.value == 5);
}
{
auto *transaction = manager.createTransaction<AdditionAction>();
// modify the action multiple times before finalizing -
// the action should be immediately applied, but undone
// when replaced.
transaction->setAction(AdditionAction(3));
assert(manager.value == 8);
transaction->setAction(AdditionAction(5));
assert(manager.value == 10);
transaction->setAction(AdditionAction(3));
transaction->finalize();
assert(manager.value == 8);
}
manager.undo();
assert(manager.value == 5);
manager.undo();
assert(manager.value == 0);
manager.redo();
assert(manager.value == 5);
manager.undo();
assert(manager.value == 0);
manager.redo();
assert(manager.value == 5);
manager.redo();
assert(manager.value == 8);
}