6
\$\begingroup\$

I've implemented a basic TransactionManager class that manages undoing/redoing of arbitrary Actions which have access to the TransactionManager's type via template parameter.

Here's the basic usage:

#include "Transaction.h"

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

To provide such a clean interface, I had to use some tricks to work around the restrictions of templates.
For example, I implemented the ActionPerformer which stores an implementation of Action inside a std::any. This way, I can store ActionPerformers containing different kinds of actions inside the same vector, which wouldn't be possible if the type of the Action was a template parameter of the ActionPerformer. The ActionPerformer is able to convert the std::any back into the appropiate Action type because the type is stored inside the lambda functions passed to it during construction, as can be seen around line 185.

In line 176, I take an unused pointer to an instance of the ActionType template class parameter, to be able to implicitly provide the TransactionType class parameter in line 69, as its own type isn't available at to the caller.

Is there an cleaner way to achieve the same results for both these "hacks"? Here's the source code of the transaction system:

#pragma once
// Transaction.h

#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>>(&currentTransaction);
    };

    /**
     * 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>

Here's an ideone page where you can run the code yourself.

\$\endgroup\$
6
\$\begingroup\$

If you want to implement the command manager pattern, you can do something so very much simpler that I don't really understand what you're aiming for.

As you know, the design pattern has a Command class exposing methods with void execute() and void undo() signatures. With C++, it can be abbreviated to:

using Command = std::pair<std::function<void()>, std::function<void()>>;

where the first function is the execute method, and the second undo.

With lambdas, which can capture anything, you can write on the fly any do/undo couple and build a Command from it.

Then your Manager is just a glorified vector which keeps track of the last executed function (a vector does as well as two stacks in this instance):

class Manager {

    std::vector<Command> timeline;
    std::vector<Command>::iterator last_command = timeline.end();

    public:
    void execute(const Command& cmd) {
        cmd.first();
        if (last_command != timeline.end())
            timeline.erase(last_command, timeline.end());
        timeline.push_back(cmd);
        last_command = timeline.end();
    }

    void undo() {
        if (last_command == timeline.begin()) return;
        (--last_command)->second();
    }

    void redo() {
        if (last_command == timeline.end()) return;
        (last_command++)->first();
    }

};

The basic usage is still more basic:

auto say(const std::string& s) {
    return Command{
        [s]() { std::cout << s << std::endl; },
        [s]() { std::cout << "Oh no, I didn't mean " << s << std::endl; }};
}

int main() {
    Manager m;
    m.execute(say("Hello!"));
    m.execute(say("You're an i....t!"));
    m.undo();
    m.redo();
    m.undo();
    m.execute(say("You're brilliant!"));
    m.undo();
    m.undo();
    m.redo();
}

I understand that it isn't really a review of your code (by the way, your code is quite good at first glance, outside of the lack of a virtual destructor for your Action class). But I'm hoping to expand on my answer once you've provide a bit more context and explain the difference between what you want to do and the command manager pattern.

A complete example for copy-and-paste ease:

#include <functional>
#include <vector>
#include <string>
#include <iostream>

using Command = std::pair<std::function<void()>, std::function<void()>>;

class Manager {

    std::vector<Command> timeline;
    std::vector<Command>::iterator last_command = timeline.end();

    public:
    void execute(const Command& cmd) {
        cmd.first();
        if (last_command != timeline.end())
            timeline.erase(last_command, timeline.end());
        timeline.push_back(cmd);
        last_command = timeline.end();
    }

    void undo() {
        if (last_command == timeline.begin()) return;
        (--last_command)->second();
    }

    void redo() {
        if (last_command == timeline.end()) return;
        (last_command++)->first();
    }

};

auto say(const std::string& s) {
    return Command{
        [s]() { std::cout << s << std::endl; },
        [s]() { std::cout << "Oh no, I didn't mean " << s << std::endl; }};
}

int main() {
    Manager m;
    m.execute(say("Hello!"));
    m.execute(say("You're an i....t!"));
    m.undo();
    m.redo();
    m.undo();
    m.execute(say("You're brilliant!"));
    m.undo();
    m.undo();
    m.redo();
}
\$\endgroup\$
  • \$\begingroup\$ Hmm, i....t - insect? \$\endgroup\$ – Toby Speight Sep 13 '18 at 13:38
  • \$\begingroup\$ @TobySpeight: or... inkpot? \$\endgroup\$ – papagaga Sep 13 '18 at 13:41
  • 1
    \$\begingroup\$ I think "insult" is the best match (grep -x i....t /usr/share/dict/words). \$\endgroup\$ – Toby Speight Sep 13 '18 at 13:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.