Patterns
Patterns aim for removing unnecessary boilerplate code from your project for commonly used programming patterns.
Visitor Pattern
Visitors allow you to separate algorithms from your data structures in object hierarchy. It allows you to implement new functionality for existing classes without modifying them (i.e., adding new virtual methods). See Wikipedia for more details.
To remove boilerplate, you can use Atoms.
Consider a simple class hiearchy of animals:
class Animal {
public:
virtual ~Animal() = default;
};
class Cat: public Animal {};
class Dog: public Animal {};
To make them visitable via atoms, you need to modify your definition a little bit:
class Cat; // Forward declaration, so we can define Visitor
class Dog; // Forward declaration, so we can define Visitor
// Our visitor base class has to be defined before the base class, so we can
// specify the visitor type in the definition
using AnimalVisitor = atoms::Visits< Cat, Dog >;
class Animal: public atoms::VisitableBase< Animal, AnimalVisitor > {
public:
virtual ~Animal() = default;
};
class Cat: public atoms::Visitable< Animal, Cat > {}; // Make cat visitable
class Dog: public atoms::Visitable< Animal, Dog > {}; // Make dog visitable
Now you can hand-craft a traditional visitor and use it:
class TypeVisitor: public AnimalVisitor {
public:
void operator()( Cat& a ) override {
result = "cat";
}
void operator()( Dog& b ) override {
result = "dog";
}
std::string result;
};
// ...and when you want to use it:
Cat cat;
Dog dog;
Animal *hiddenCat = &cat;
Animal *hiddenDog = &dog;
TypeVisitor visitor;
cat.accept( visitor );
std::cout << visitor.result << "\n";
dog.accept( visitor );
std::cout << visitor.result << "\n";
hiddenCat->accept( visitor );
std::cout << visitor.result << "\n";
hiddenDog->accept( visitor );
std::cout << visitor.result << "\n";
With atoms, you can even construct a visitor in place from an ordinary function or/and lambdas:
std::string catType( Cat& ) { return "cat"; }
auto visitor = AnimalVisitor::make(
catType, // Cat is visited by an ordinary function
[]( Dog& ) -> std::string { return "dog" }); // Dog is visited by a lambda
// Note that you have to cover all cases, otherwise your program won't compile
// Then we can use it like before
hiddenCat->accept( visitor );
std::cout << visitor.result << "\n";
This is not all, with Atoms, you can avoid the annoying “accept then result” construction and visit object like function call:
std::cout << atoms::visit( hiddenCat, visitor) << "\n";
// You can even define the visitor in place:
std::cout << atoms::visit( hiddenCat,
[]( Cat& cat ) { return "cat"; },
[]( Dog& dog ) { return "dog"; } ) << "\n";
Bellow, you can find the references of the provided classes and functions:
-
template<typename ...Hosts>
struct Visits : public atoms::detail::Visits<Hosts...> Visitor type for given set of derived classes (Hosts).
Typical use case is
using MyTypeVisitor = atoms::Visits< Derived1, Derived2 >
Subclassed by UniversalModuleJointVisitor
-
template<typename Self, typename Visitor>
struct VisitableBase Make a base class visitable.
This is a CRTP class, inherit from it in your base class and pass the base class type and a base visitor type as template arguments. The base visitor type should be created as a using to Visits.
Typical use case
class Base: public atoms::VisitableBase< Base, BaseVisitor > {};
-
template<typename Base, typename Self>
struct Visitable : public Base Implement visits interface in derived class.
This is a CRTP class, inherit from it in you class and pass base class and your derived class as the template arguments.
Typical use case
class Derived: public Visitable< Base, Derived > {};
-
template<typename Base, typename F, typename ...Fs>
struct Visitor : public atoms::detail::Visitor<Base, FunctionTraits<F>::returnType, F, Fs...> This type takes a desired visitor type (created by templateing Visits), list of callables and produces a visitor out of the callable.
Prefer calling Visits::make() for creating instances as it automatically deduces all the types.
Public Types
-
using Successor = detail::Visitor<Base, ReturnType, F, Fs...>
-
using Successor = detail::Visitor<Base, ReturnType, F, Fs...>
-
template<typename T, typename ...Fs>
auto atoms::visit(T &object, Fs... fs) Visit given object by se of callables.
Expects that T inherits from atoms::VisitableBase.
Expects that all callables have the same return value and each of them accepts exactly one overload of T.
-
template<typename T, typename Visitor>
auto atoms::visit(T &object, Visitor visitor) -> std::enable_if_t<std::is_base_of_v<typename T::VisitorType, Visitor>, typename Visitor::ReturnType> Visit given object by a visitor.
Gives visiting the same interface as std::visits provides and removes the hassle of extracting the return value out of the visitor.
Expects that T inherits from atoms::VisitableBase.
Expects that Visitor::ReturnType and Visitor::result (in case of non-void return type) exists. Both of these are guaranteed if atoms::Visitor is used.
Cloneable Object Hiearchy & ValuePtr
Atoms provide means to simply implement cloneable virtual hiearchies. Consider the following code:
class Animal {
public:
virtual ~Animal() = default;
};
class Dog: public Animal {};
class Cat: public Animal {};
// Our code:
std::unique_ptr< Animal > ptr( new Dog() );
// Let's make a copy - how to do so? We don't know what is behind Animal.
// The following attempt won't work:
std::unique_ptr< Animal > ptrCopy( new Animal( *ptr.get() ) ); // Does not compile
To make the it work we can add a virtual clone
method. This process can be
tedious, so Atoms provide several macros to easy the task. By the way, we use
macro-based approach over CRTP-based approach as we consider it more readable
and easier to maintain.
class Animal {
public:
virtual ~Animal() = default;
ATOMS_CLONEABLE_BASE( Animal );
};
class Dog: public Animal {
public:
ATOMS_CLONEABLE( Dog );
};
class Cat: public Animal {
public:
ATOMS_CLONEABLE( Cat );
};
// Our code:
std::unique_ptr< Animal > ptr( new Dog() );
// Let's make a copy:
std::unique_ptr< Animal > ptrCopy( ptr->clone() );
Sometimes, you need to store a such hierarchy in class which you want to make
copyable. With unique_ptr
you have to manually define all the copy and move
constructors. This can be tedious, therefore, atoms provide
atoms::ValuePtr
which stores objects on heap, but provides copy
semantics for them. It is somewhat similar to proposed std::polymorphic_value
,
however, it does not feature small-buffer optimization to provide stable
references.
-
template<typename T>
class ValuePtr Store object on a heap (to allow virtual function calls and to provide stable address on move) and provide deep copies.
It requires that T provides
T *clone() const
. UseATOMS_CLONEABLE_BASE
andATOMS_CLONEABLE
for providing this method.