Skip to content

Instantly share code, notes, and snippets.

@tomeichlersmith
Last active January 26, 2022 20:35
Show Gist options
  • Save tomeichlersmith/017ae50b9c1c35afa227a4bebde933b9 to your computer and use it in GitHub Desktop.
Save tomeichlersmith/017ae50b9c1c35afa227a4bebde933b9 to your computer and use it in GitHub Desktop.
A factory for dynamically creating calsses derived from a "prototype" by name.

Factory

A factory for dynamically creating classes derived from a "prototype" by name.

Terminology

  • Factory: A object that has a look-up table between class names and pointers to functions that can create them
  • Maker: A function that can create a specific class
  • Prototype: A abstract base class from which derived classes can be used

Design

The factory itself works on two steps.

First, all of the different derived classes "register" or "declare" themselves so that the factory knows how to create them. This registration is done by providing a function that can construct them in association with the name of the derived class.

Second, the factory creates any of the registered classes and return a pointer to it in the form of a prototype-class pointer.

Usage

Using the factory effectively can be done in situations where many classes all follow the same design structure but have different implementations for specific steps. In order to reflect this "same design structure", we define an abstract base class (a class with at least one pure virtual function) for all of our derived classes to inherit from. This abstract base class is our "prototype".

// LibraryEntry.hpp
#ifndef LIBRARYENTRY_HPP
#define LIBRARYENTRY_HPP
// we need the factory template
#include "Factory/Factory.hpp"

// this class is our prototype
class LibraryEntry {
 public:
  // virtual destructor so we can dynamically create derived classes
  virtual ~LibraryEntry() = default;
  // pure virtual function that our derived classes will implement
  virtual std::string name() = 0;
  // the factory type that we will use here
  using Factory = ::factory::Factory<LibraryEntry>;
};  // LibraryEntry

// a macro to help with registering our library entries with our factory
#define DECLARE_LIBRARYENTRY(CLASS)                                 \
   namespace {                                                      \
     auto v = ::LibraryEntry::Factory::get().declare<CLASS>(#CLASS) \
   }
#endif // LIBRARYENTRY_HPP

This LibraryEntry prototype satisfies our requirements. Now, we can define several other library entries in other source files.

// Book.cpp
#include "LibraryEntry.hpp"
namespace library {
class Book : public LibraryEntry {
 public :
  virtual std::string name() final override {
    return "Where the Red Fern Grows";
  }
};
}

DECLARE_LIBRARYENTRY(library::Book)
// Podcast.cpp
#include "LibraryEntry.hpp"
namespace library {
namespace audio {
class Podcast : public LibraryEntry {
 public :
  virtual std::string name() final override {
    return "538 Politics Podcast";
  }
};
}
}

DECLARE_LIBRARYENTRY(library::audio::Podcast)
// Album.cpp
#include "LibraryEntry.hpp"
namespace library {
namespace audio {
class Album : public LibraryEntry {
 public :
  virtual std::string name() final override {
    return "Kind of Blue";
  }
};
}
}

DECLARE_LIBRARYENTRY(library::audio::Album)

Since we use an anonymous namespace to declare the various library entries, the dummy variables that we are defining are static and therefore will be constructed during library load. Since they are constructed from the output of the factory declaration function, we are able to register our before the main program even begins.

#include "LibraryEntry.hpp"

int main(int argc, char* argv[]) {
  std::string full_cpp_name{argv[1]}; 
  auto entry_ptr{LibraryEntry::Factory::get().make(full_cpp_name)};
  std::cout << entry_ptr->name() << std::endl;
}

Compiling this main into the favorite-things executable would then lead to the behavior.

$ favorite-things library::Book
Where the Red Fern Grows
$ favorite-things library::audio::Podcast
538 Politics Podcast
$ favorite-things library::audio::Album
Kind of Blue
#ifndef FACTORY_FACTORY_HPP
#define FACTORY_FACTORY_HPP
#include <exception> // to throw not found exceptions
#include <memory> // for the unique_ptr default
#include <string> // for the keys in the library map
#include <unordered_map> // for the library
/**
* factory namespace
*
* This namespace is used to isolate the templated Factory
* from where other Factories are defined. There should be
* nothing else in this namespace in order to avoid potential
* name conflicts.
*/
namespace factory {
/**
* Factory to dynamically create objects derived from a specific prototype
* class.
*
* This factory is a singleton class meaning it cannot be created by the user.
*
* The factory has three template parameters in order of complexity.
* 1. Prototype - REQUIRED - the type of object that this factory creates.
* This should be the base class that all types in this factory derive from.
* 2. PrototypePtr - optional - the type of pointer to object
* By default, we use std::unique_ptr for good memory management.
* 3. PrototypeMakerArgs - optional - type of objects passed into the object
* maker i.e. same as arguments to the constructor used by the object maker
*
* In order to save code repetition, it is suggested to alias
* your specific factory in your own namespace. This allows you to control
* all the template inputs for your factory in one location.
*
* using MyPrototypeFactory = factory::Factory<MyPrototype>;
*
* Or, if you are in some other namespace, you can shorten it even more.
*
* namespace foo {
* using Factory = factory::Factory<MyPrototype>;
* }
*/
template <typename Prototype,
typename PrototypePtr = std::unique_ptr<Prototype>,
typename... PrototypeMakerArgs>
class Factory {
public:
/**
* the signature of a function that can be used by this factory
* to dynamically create a new object.
*
* This is merely here to make the definition of the Factory simpler.
*/
using PrototypeMaker = PrototypePtr (*)(PrototypeMakerArgs...);
public:
/**
* get the factory
*
* Using a static function variable gaurantees that the factory
* is created as soon as it is needed and that it is deleted
* before the program completes.
*/
static Factory& get() {
static Factory the_factory;
return the_factory;
}
/**
* register a new object to be constructible
*
* We insert the new object into the library after
* checking that it hasn't been defined before.
*
* We throw a runtime_error exception if the object has been declared before.
* This exception can easily be avoided by making sure the declaration
* macro for a prototype links the name of the PrototypeMaker function to
* the name of the derived class. This means the user would have a
* compile-time error rather than a runtime exception.
*
* full_name - name to use as a reference for the declared object
* maker - a pointer to a function that can dynamically create an instance
*/
template<typename DerivedType>
uint64_t declare(const std::string& full_name) {
auto lib_it{get().library_.find(full_name)};
if (lib_it != library_.end()) {
throw Exception("Factory",
"An object named " + full_name +
" has already been declared.",false);
}
library_[full_name] = &maker<DerivedType>;
return reinterpret_cast<std::uintptr_t>(&library_);
}
/**
* make a new object by name
*
* We look through the library to find the requested object.
* If found, we create one and return a pointer to the newly
* created object. If not found, we raise an exception.
*
* The arguments to the maker are determined at compiletime
* using the template parameters of Factory.
*
* full_name - name of object to create, same name as passed to declare
* maker_args - parameter pack of arguments to pass on to maker
*
* Returns a pointer to the parent class that the objects derive from.
*/
PrototypePtr make(const std::string& full_name,
PrototypeMakerArgs... maker_args) {
auto lib_it{library_.find(full_name)};
if (lib_it == library_.end()) {
throw std::runtime_error("An object named " + full_name +
" has not been declared.");
}
return lib_it->second(maker_args...);
}
/// delete the copy constructor
Factory(Factory const&) = delete;
/// delete the assignment operator
void operator=(Factory const&) = delete;
private:
/**
* make a new DerivedType returning a PrototypePtr
*
* Basically a copy of what
* [`std::make_unique`](https://en.cppreference.com/w/cpp/memory/unique_ptr/make_unique)
* or
* [`std::make_shared`](https://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared)
* do but with the following changes:
* 1. constructor arguments defined by the Factory and not here
* 2. return type is a base pointer and not a derived pointer
*
* This is where we required that PrototypePtr has the same
* behavior as STL smart pointers. The PrototypePtr class must
* be able to be constructed from a pointer to a derived class
* and must take ownership of the new object.
*
* @tparam DerivedType type of derived object we should create
* @param[in] args constructor arguments for derived type construction
*/
template <typename DerivedType>
static PrototypePtr maker(PrototypeConstructorArgs... args) {
return PrototypePtr(new DerivedType(std::forward<PrototypeConstructorArgs>(args)...));
}
/// private constructor to prevent creation
Factory() = default;
/// library of possible objects to create
std::unordered_map<std::string, PrototypeMaker> library_;
}; // Factory
} // namespace factory
#endif // FACTORY_FACTORY_HPP
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment