Skip to content

Instantly share code, notes, and snippets.

@thierryseegers
Last active July 10, 2016 09:52
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save thierryseegers/9027924 to your computer and use it in GitHub Desktop.
Save thierryseegers/9027924 to your computer and use it in GitHub Desktop.
commands is a single-header C++ framework that reifies the command design pattern. It features macro recording, multi-level undo and serialization.
Dummy file for the sake of having a good name for this gist.
/*
(C) Copyright Thierry Seegers 2014. Distributed under the following license:
Boost Software License - Version 1.0 - August 17th, 2003
Permission is hereby granted, free of charge, to any person or organization
obtaining a copy of the software and accompanying documentation covered by
this license (the "Software") to use, reproduce, display, distribute,
execute, and transmit the Software, and to prepare derivative works of the
Software, and to permit third-parties to whom the Software is furnished to
do so, all subject to the following:
The copyright notices in the Software and this entire statement, including
the above license grant, this restriction and the following disclaimer,
must be included in all copies of the Software, in whole or in part, and
all derivative works of the Software, unless such copies or derivative
works are solely in the form of machine-executable object code generated by
a source language processor.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
#include "commands.h"
#if defined(COMMANDS_SUPPORT_BOOST_SERIALIZATION)
#if defined(_MSC_VER)
#pragma warning(push)
#pragma warning(disable : 4244)
#endif
#include <boost/archive/binary_iarchive.hpp>
#if defined(_MSC_VER)
#pragma warning(pop)
#endif
#include <boost/archive/binary_oarchive.hpp>
#include <boost/archive/text_iarchive.hpp>
#include <boost/archive/text_oarchive.hpp>
#include <boost/serialization/export.hpp>
#endif
//!\cond
#if defined(COMMANDS_SUPPORT_BOOST_SERIALIZATION)
BOOST_CLASS_EXPORT(commands::macro_command);
BOOST_CLASS_EXPORT(commands::detail::history_t);
#endif
//!\endcond
/*!
\file commands.cpp
\brief Add this file to your project if serialization is enabled.
This file is not necessary if serialization is not enabled.
*/
/*
(C) Copyright Thierry Seegers 2014. Distributed under the following license:
Boost Software License - Version 1.0 - August 17th, 2003
Permission is hereby granted, free of charge, to any person or organization
obtaining a copy of the software and accompanying documentation covered by
this license (the "Software") to use, reproduce, display, distribute,
execute, and transmit the Software, and to prepare derivative works of the
Software, and to permit third-parties to whom the Software is furnished to
do so, all subject to the following:
The copyright notices in the Software and this entire statement, including
the above license grant, this restriction and the following disclaimer,
must be included in all copies of the Software, in whole or in part, and
all derivative works of the Software, unless such copies or derivative
works are solely in the form of machine-executable object code generated by
a source language processor.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
#if !defined(COMMANDS_H)
#define COMMANDS_H
#if defined(COMMANDS_SUPPORT_BOOST_SERIALIZATION)
#if defined(_MSC_VER)
#pragma warning(push)
#pragma warning(disable : 4244)
#endif
#include <boost/archive/binary_iarchive.hpp>
#if defined(_MSC_VER)
#pragma warning(pop)
#endif
#include <boost/archive/binary_oarchive.hpp>
#include <boost/archive/text_iarchive.hpp>
#include <boost/archive/text_oarchive.hpp>
#include <boost/serialization/access.hpp>
#include <boost/serialization/assume_abstract.hpp>
#include <boost/serialization/base_object.hpp>
#include <boost/serialization/list.hpp>
#include <boost/serialization/shared_ptr.hpp>
#endif
#include <algorithm>
#include <functional>
#include <iterator>
#include <list>
#include <memory>
#include <type_traits>
//!\brief Contains all classes and functions to implement a retraceable command history.
namespace commands
{
//!\brief Commands that can't be undone must derive from this base class.
//!
//! These commands will not be kept in history.
class command
{
#if defined(COMMANDS_SUPPORT_BOOST_SERIALIZATION)
friend class boost::serialization::access;
//!\brief Serializes and de-serializes to and from an archive.
template<class A>
void serialize(A&, const unsigned int)
{}
#endif
public:
virtual ~command() {}
//!\brief The action that this command performs.
virtual void execute() = 0;
};
//!\cond
#if defined(COMMANDS_SUPPORT_BOOST_SERIALIZATION)
BOOST_SERIALIZATION_ASSUME_ABSTRACT(command);
#endif
//!\endcond
//!\brief Undoable commands must derive from this base class.
//!
//! These commands will be kept in history.
class reversible_command : public command
{
#if defined(COMMANDS_SUPPORT_BOOST_SERIALIZATION)
friend class boost::serialization::access;
//!\brief Serializes and de-serializes to and from an archive.
template<class A>
void serialize(A& archive, const unsigned int)
{
archive & boost::serialization::base_object<command>(*this);
}
#endif
public:
virtual ~reversible_command() {}
//!\brief Reverts this command's previously performed action.
virtual void undo() = 0;
};
//!\cond
#if defined(COMMANDS_SUPPORT_BOOST_SERIALIZATION)
BOOST_SERIALIZATION_ASSUME_ABSTRACT(reversible_command);
#endif
//!\endcond
//!\brief Implementation details.
namespace detail
{
//!\typedef reversible_command_ptr
//!\brief Type that holds a pointer to a \ref reversible_command.
#if defined(COMMANDS_SUPPORT_BOOST_SERIALIZATION)
typedef boost::shared_ptr<reversible_command> reversible_command_ptr;
#else
typedef std::shared_ptr<reversible_command> reversible_command_ptr;
#endif
typedef std::list<reversible_command_ptr> timeline_t; //!< A sequence of commands.
}
//!\brief A sequence of commands.
//!
//! A macro command consists of a sequence of \ref reversible_command instances, possibly including other macros.
class macro_command : public reversible_command
{
public:
macro_command()
{}
//!\brief Constructor which takes a timeline.
macro_command(detail::timeline_t timeline) : timeline_(timeline)
{}
virtual ~macro_command() {}
//!\brief Re-executes its command sequence.
virtual void execute() override
{
std::for_each(timeline_.begin(), timeline_.end(), std::mem_fn(&command::execute));
}
//!\brief Reverts its command sequnce.
virtual void undo() override
{
std::for_each(timeline_.rbegin(), timeline_.rend(), std::mem_fn(&reversible_command::undo));
}
private:
detail::timeline_t timeline_; //!< The sequence of \ref reversible_command.
#if defined(COMMANDS_SUPPORT_BOOST_SERIALIZATION)
friend class boost::serialization::access;
//!\brief Serializes and de-serializes to and from an archive.
template<class A>
void serialize(A& archive, const unsigned int)
{
archive & boost::serialization::base_object<commands::reversible_command>(*this);
archive & timeline_;
}
#endif
};
namespace detail
{
//!\brief Implements the command history.
//!
//! An iterator is maintained to the point of command insertion in the history sequence.
//! When calling \ref execute, the command is inserted at the iterator and the iterator is incremented.
//! When calling \ref undo, the iterator is decremented and the command to which it points is reverted by calling \ref reversible_command::undo.
//! When calling \ref redo, the command to which the iterator points to is re-executed by calling \ref command::execute and the iterator is incremented.
class history_t
{
public:
//!\brief Default constructor.
history_t() : now_(timeline_.end()), macro_begin_(0) {}
//!\brief Executes a command and, if the command is reversible, adds it to the history.
template<typename C, typename... Args>
void execute(Args&&... args)
{
execute(std::move(C(std::forward<Args>(args)...)));
}
//!\brief Executes a command and, if the command is reversible, adds it to the history.
template<typename C>
void execute(C&& c)
{
c.execute();
if(std::is_base_of<reversible_command, typename std::remove_reference<C>::type>::value)
{
remember(c);
}
}
//!\brief Reverts a command.
//!
//! If a macro is being recorded, will not go prior to the macro's starting point.
//!\return \c true if more commands can be undone.
bool undo()
{
if(now_ != timeline_.begin() && std::distance(timeline_.cbegin(), now_) > macro_begin_)
{
(*--now_)->undo();
}
return now_ != timeline_.begin() && std::distance(timeline_.cbegin(), now_) > macro_begin_;
}
//!\brief Re-executes a command.
//!\return \c true if more commands can be re-executed.
bool redo()
{
if(now_ != timeline_.cend())
{
(*now_++)->execute();
}
return now_ != timeline_.cend();
}
//!\brief Starts recording a macro.
//!
//! Commands executed while recording a macro will still be added to the history.
void begin_macro()
{
macro_begin_ = std::distance(timeline_.cbegin(), now_);
}
//!\brief Ends recording a macro.
//!\return The \ref macro_command created.
macro_command end_macro()
{
auto i = timeline_.cbegin();
std::advance(i, macro_begin_);
macro_begin_ = 0;
return detail::timeline_t(i, now_);
}
private:
detail::timeline_t timeline_; //!< A sequence of \ref reversible_command.
detail::timeline_t::const_iterator now_; //!< The insertion point for the next command.
detail::timeline_t::const_iterator::difference_type macro_begin_; //!< The starting point of the macro currently being recorded.
template<typename C>
void remember(C&& c)
{
if(now_ != timeline_.cend())
{
now_ = timeline_.erase(now_, timeline_.cend());
}
timeline_.emplace(now_, new typename std::remove_reference<C>::type(std::forward<C>(c)));
}
#if defined(COMMANDS_SUPPORT_BOOST_SERIALIZATION)
friend class boost::serialization::access;
//!\brief Serializes to an archive.
template<class A>
void save(A& archive, const unsigned int) const
{
archive & timeline_;
auto d = std::distance(timeline_.begin(), now_);
archive & d;
archive & macro_begin_;
}
//!\brief Deserializes from an archive.
template<class A>
void load(A& archive, const unsigned int)
{
archive & timeline_;
detail::timeline_t::const_iterator::difference_type d;
archive & d;
std::advance(now_ = timeline_.begin(), d);
archive & macro_begin_;
}
BOOST_SERIALIZATION_SPLIT_MEMBER();
#endif
};
}
static detail::history_t history; //!< The entire history of commands executed.
}
#endif
/*!
\file commands.h
\brief Holds all facilities you need to integrate command history in your codebase.
\author Thierry Seegers
\version 1.1
\mainpage commands
\tableofcontents
\section description Description
\ref commands is a single-header C++ framework that reifies the <a href="http://en.wikipedia.org/wiki/Command_pattern">command design pattern</a>.
\ref commands implements three extra features:
multi-level <a href="http://en.wikipedia.org/wiki/Undo">undo</a>,
<a href="http://en.wikipedia.org/wiki/Macro_%28computer_science%29">macro recording</a> and
<a href="http://en.wikipedia.org/wiki/Serialization">serialization</a>.
\ref commands instantiates a static \ref commands::detail::history_t "history" object that is the single point of interaction with the framework.
To use the facilities in \ref commands, non-reversible command classes must derive from \ref commands::command and reversible commands must derive from \ref commands::reversible_command.
To execute your commands, call either versions of \ref commands::detail::history_t::execute "commands::history.execute".
\subsection multi_level_undo Multi-level undo
Command classes that derive from \ref commands::reversible_command will be kept in history after being executed.
Commands in history can be reverted or re-executed by calling \ref commands::detail::history_t::undo "commands::history.undo" or \ref commands::detail::history_t::redo "commands::history.redo" respectively.
\subsection macro Macro commands
To start recording a sequence a commands for later re-use, call \ref commands::detail::history_t::begin_macro "commands::history.begin_macro".
To stop recording and save the result, assign a variable to the result of calling \ref commands::detail::history_t.end_macro "commands::history.end_macro".
Macros can be executed and added to the history like any other reversible command.
Macros can contain other macros. i.e. you can execute macros between calls to \ref commands::detail::history_t::begin_macro "commands::history.begin_macro" and \ref commands::detail::history_t.end_macro "commands::history.end_macro".
\subsection serialization Serialization
This features allows to save the current history of commands to a stream.
Serialization is implemented using <a href="http://www.boost.org/doc/libs/1_55_0/libs/serialization/doc/index.html">Boost.Serialize</a> and is enabled by defining \c COMMANDS_SUPPORT_BOOST_SERIALIZATION before including this file.
All the classes in \ref commands implement the plumbing required by the Boost.Serialization framework to allow them to be serialized.
You must implement similar functionality in your own classes.
See the sample code for an example of serialized history and macro commands.
\note If serialization is enabled, add \ref commands.cpp in your project.
\section sample Sample code
\include example.cpp
\section license License
\verbatim
(C) Copyright Thierry Seegers 2014. Distributed under the following license:
Boost Software License - Version 1.0 - August 17th, 2003
Permission is hereby granted, free of charge, to any person or organization
obtaining a copy of the software and accompanying documentation covered by
this license (the "Software") to use, reproduce, display, distribute,
execute, and transmit the Software, and to prepare derivative works of the
Software, and to permit third-parties to whom the Software is furnished to
do so, all subject to the following:
The copyright notices in the Software and this entire statement, including
the above license grant, this restriction and the following disclaimer,
must be included in all copies of the Software, in whole or in part, and
all derivative works of the Software, unless such copies or derivative
works are solely in the form of machine-executable object code generated by
a source language processor.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
\endverbatim
*/
#include "commands.h"
#if defined(COMMANDS_SUPPORT_BOOST_SERIALIZATION)
#include <boost/archive/binary_iarchive.hpp>
#include <boost/archive/binary_oarchive.hpp>
#include <boost/serialization/access.hpp>
#include <boost/serialization/base_object.hpp>
#include <boost/serialization/export.hpp>
#endif
#include <fstream>
#include <iostream>
using namespace std;
class A : public commands::reversible_command
{
int i_;
#if defined(COMMANDS_SUPPORT_BOOST_SERIALIZATION)
friend class boost::serialization::access;
template<class A>
void serialize(A& archive, const unsigned int)
{
archive & boost::serialization::base_object<commands::reversible_command>(*this);
archive & i_;
}
#endif
public:
A(int i = 0) : i_(i) {}
virtual ~A() {}
virtual void execute() override
{
cout << "Executing A, i = " << i_ << endl;
}
virtual void undo() override
{
cout << "Undoing A, i = " << i_ << endl;
}
};
#if defined(COMMANDS_SUPPORT_BOOST_SERIALIZATION)
BOOST_CLASS_EXPORT(A);
#endif
class B : public commands::reversible_command
{
float f_;
#if defined(COMMANDS_SUPPORT_BOOST_SERIALIZATION)
friend class boost::serialization::access;
template<class A>
void serialize(A& archive, const unsigned int)
{
archive & boost::serialization::base_object<commands::reversible_command>(*this);
archive & f_;
}
#endif
public:
B(float f = 0.f) : f_(f) {}
virtual ~B() {}
virtual void execute() override
{
cout << "Executing B, f = " << f_ << endl;
}
virtual void undo() override
{
cout << "Undoing B, f = " << f_ << endl;
}
};
#if defined(COMMANDS_SUPPORT_BOOST_SERIALIZATION)
BOOST_CLASS_EXPORT(B);
#endif
int main()
{
cout << "Executing commands, undoing and redoing.\n";
// Execute two instances of class A with different parameters.
commands::history.execute<A>(1);
commands::history.execute(A(2));
// Undo once.
commands::history.undo();
// Undo until there is nothing left to undo. Will undo once because we have previously undone the second B.
while(commands::history.undo());
// Redo until there is nothing left to redo. Will redo twice because there are two instances of B in the history.
while(commands::history.redo());
// Start recording a macro.
cout << "\nRecording macro m1.\n";
commands::history.begin_macro();
commands::history.execute(B(1.1f));
commands::history.execute(A(3));
// End recording and save the result.
auto m1 = commands::history.end_macro();
// Execute that macro.
cout << "\nExecuting macro m1.\n";
commands::history.execute(m1);
// Undo the last command, which happens to be a macro.
cout << "\nUndo-ing macro m1.\n";
commands::history.undo();
// Recording macro m2, which will consist of macro m1 followed by an instance of A.
cout << "\nRecording macro m2, which will include macro m1.\n";
commands::history.begin_macro();
commands::history.redo();
commands::history.execute(A(4));
auto m2 = commands::history.end_macro();
// Execute macro m2.
cout << "\nExecuting macro m2.\n";
commands::history.execute(m2);
#if defined(COMMANDS_SUPPORT_BOOST_SERIALIZATION)
// Serialize history.
cout << "\nSaving history to archive.\n";
std::ofstream ofs("stuff");
boost::archive::binary_oarchive bato(ofs);
bato << commands::history;
cout << "\nSaving macro m1 to the same archive.\n";
bato << m1;
cout << "\nSaving macro m2 to the same archive.\n";
bato << m2;
ofs.flush();
// De-serialize history.
cout << "\nLoading history from archive.\n";
std::ifstream ifs("stuff");
boost::archive::binary_iarchive bati(ifs);
bati >> commands::history;
#endif
// Undo and redoing everything.
cout << "Undoing everything.\n";
while(commands::history.undo());
cout << "Redoing everything.\n";
while(commands::history.redo());
#if defined(COMMANDS_SUPPORT_BOOST_SERIALIZATION)
// Load macro m1 from same archive and execute it.
commands::macro_command m11;
cout << "\nLoading and executing macro m1 from same archive.\n";
bati >> m11;
m11.execute();
// Load macro m2 from same archive and execute it.
commands::macro_command m22;
cout << "\nLoading and executing macro m2 from same archive.\n";
bati >> m22;
m22.execute();
#endif
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment