CPP / C++ Notes - Design Patterns
Table of Contents
- 1. General Design Patterns
- 1.1. General Techniques / Mechanism for Code Reuse
- 1.2. Singleton
- 1.3. Named constructor - static factory method
- 1.4. Factory Method
- 1.5. Builder (Joshua Blosh)
- 1.6. Decorator
- 1.7. Strategy
- 1.8. Method Chaining - Fluent API
- 1.9. GOF - Template Pattern
- 1.10. Null-Object Pattern
- 1.11. Composite
- 1.12. Observer-Observable
- 1.13. Chain of Responsibility
- 1.14. Visitor
- 2. C++ Specific Design Pattern and Idioms
- 2.1. Erase-Remove Idiom
- 2.2. Virtual Friend Function Idiom
- 2.3. RAII idiom - Resource Aquisition Is Initialization
- 2.4. PIMPL idiom - Pointer to Implementation
- 2.5. Non-copiable class idiom
- 2.6. Copy and swap idiom
- 2.7. Interface Class idiom
- 2.8. CRTP - Curious Recurring Template Pattern
- 2.9. Type Erasure Pattern
- 2.10. Virtual Copy Constructor Idiom - Prototype Pattern
- 2.11. QT Parent-child memory management
- 2.12. Cross platform code with conditional compilation
- 3. Misc Techniques
1 General Design Patterns
1.1 General Techniques / Mechanism for Code Reuse
- Inheritance
- Dynamic / Runtime / Subtyping Polymorphism.
- Benefits:
- Allow switching implementation at runtime, store different objects (pointers, smart pointers or references) at the same container, defer method calls to specific implementation and allow a client code work with multiple different implementations at runtime. Inheritance alone is not evil, the problem is deep inheritance hierarchies.
- Drawbacks:
- Loss of value semantics and intrusive.
- Classes must inherit from the same base class which requires modification.
- Objects can only be returned from factory functions as pointers or pointers wrapped by smart pointers.
- Slower speed than static polymorphism (templates and overloaded functions or methods.)
- Delegation (a kind of OO composition)
- Use another object for implementing some functionality and forward a method call to it instead of inheriting the object class. Examples: store items in a std::vector, instead of storing them directly in the class with heap-allocated arrays; store an heap-allocated object using smart pointers rather than storing it using raw pointer and freeing it on the destructor.
- Static Polymorphism
- It is templates and overloaded functions or methods.
- Templates can eliminate virtual method call overhead by resolving methods at compile-time rather than at runtime.
- Type Erasure
- Combination of static and dynamic polymorphism which gets the best of both sides with minimal overhead.
- Free Functions
- C++ is a multiparadigm language and not everything needs to to be a class. So, functions can be used to extending classes without modifying them by taking objects as arguments and applying operations on them.
- Higher Order Functions / Higher Order Methods / Lambda Functions
- Lambda function can be used to add new behaviors to an object at runtime, create generalized algorithms, functions, simplify event handling, callbacks and design patterns.
1.2 Singleton
1.2.1 Overview
Singleton is a creational design pattern where there is only a single instance of a class and client code is forbidden from creating new instances.
Note: Despite that there are lots of objections against this design pattern, it is still worth knowing how it works.
Use cases:
- Any class where just a single instance is needed.
- Encapsulate global states or global variables in a single global object.
- Caches
- File caches
- Logging (Logger object)
- Runtime configuration
- Configuration files
- Factory classes in factory-design pattern. A factory-object is used for indirect instatiation of objects from some class hierarchy.
- Device Driver
Implementation variants:
- Early initialized
- Lazily initialized, initialized only when needed.
Disadvantages of singleton patterns:
- Testability => It is hard to isolate code that needs the singleton and replace it at runtime.
- Can hide dependency between classes.
- Concurrency => The singleton must be made thread-safe in multi-threading applications. Access to global state without proper locking and synchronization can lead to race condition bugs.
Example where singletons are found:
- C++ Standard-library global IO object (Singletons):
- std::cin => Encapsulates console input - standard input
- std::cout => Encapsulates console stdout - standard output
- std::cerr => Encapsulates console stderr - standard error output
- std::clog
Further Reading
- Singleton pattern - Wikipedia
- The Parametric Singleton Design Pattern
- Singletons Considered Harmful
- design patterns - What is so bad about singletons? - Stack Overflow
- On design patterns: When should I use the singleton? - Stack Overflow
- Root Cause of Singletons
- Singleton Design Pattern Tutorial – An Introspection w/ Best Practices - DZone Java
- Singleton Design Pattern | Introduction - GeeksforGeeks
- Java Singleton Design Pattern Example Best Practices - JournalDev
- Singleton Pattern Pitfalls | Vojtech Ruzicka's Programming Blog
- Singleton Pattern – Positive and Negative Aspects - CodeProject
- https://wiki.unity3d.com/index.php/Singleton (Games C#)
1.2.2 Example 1 - FileRepository Class
Example Code
- File: file:src/design-patterns/singleton1.cpp
- Online Compiler: https://rextester.com/SJWB72590
- Note: This code is not thread-safe.
Header of class FileRepository => (FileRepository.hpp)
//----- file: FileRepository.hpp ------// #ifndef __file_repository #define __file_repository #include <std::string> class FileRepository { private: std::deque<std::string> _files; // Forbid client code instating a new instance. FileRepository(){ std::cerr << " [LOG] File Respository Initialized." << "\n"; } // Forbid client code from creating a copy or using the // copy constructor. FileRepository(const FileRepository&){} public: ~FileRepository(); // Return a reference to not allow client code // to delete object. static auto getInstance() -> FileRepository&; // Use old C++ 'member function' syntax. void addFile(std::string fname); void clearFiles(); // C++11 member function declaration looks better. auto showFiles() -> void; }; #endif
Implementation of FileRepository (FileRepository.cpp)
#include <iostream> #include "FileRepository.hpp" FileRepository::~FileRepository(){ std::cerr << " [LOG] File Respository Deleted. Ok." << "\n"; } // Static method auto FileRepository::getInstance() -> FileRepository& { static auto instance = std::unique_ptr<FileRepository>{nullptr}; // Initialized once - lazy initialization if(!instance) instance.reset(new FileRepository); return *instance.get(); } void FileRepository::addFile(std::string fname){ _files.push_back(std::move(fname)); } void FileRepository::clearFiles(){ _files.clear(); } // C++11 Member function declaration auto FileRepository::showFiles() -> void { for(const auto& file: _files) std::cout << " File = " << file << std::endl; }
Test in CERN ROOT/Clign REPL:
// Load C++ code as it was a script. >> .L singleton1.cpp // Try to instantiate singleton object without reference. //------------------------------------------------------ >> FileRepository repo = FileRepository::getInstance() ROOT_prompt_2:1:23: error: calling a private constructor of class 'FileRepository' FileRepository repo = FileRepository::getInstance() ^ singleton1.cpp:21:5: note: declared private here FileRepository(const FileRepository&){} ^ >> FileRepository& repo = FileRepository::getInstance() [LOG] File Respository Initialized. (FileRepository &) @0x2fc9640 >> >> repo.showFiles() >> >> repo.addFile("quarterly-sales-report.dat") >> repo.addFile("interest-payments.txt") >> repo.addFile("taxes-report.xls") >> repo.showFiles() File = quarterly-sales-report.dat File = interest-payments.txt File = taxes-report.xls >> >> // Try to copy object. >> FileRepository r = repo; ROOT_prompt_9:1:20: error: calling a private constructor of class 'FileRepository' FileRepository r = repo; ^ singleton1.cpp:21:5: note: declared private here FileRepository(const FileRepository&){} ^ >> // Try to create a new object >> FileRepository& repo2 = FileRepository::getInstance() (FileRepository &) @0x2fc9640 >> repo2.showFiles() File = quarterly-sales-report.dat File = interest-payments.txt File = taxes-report.xls >> // Check whether repo and repo2 are the same object (reference equality) // -> They are equal under reference equality criteria if they have the same address. >> &repo == &repo2 (bool) true >> // Exit REPL. >> .q [LOG] File Respository Deleted. Ok.
Main function:
FileRepository& repo1 = FileRepository::getInstance(); repo1.addFile("CashFlowStatement.txt"); repo1.addFile("Balance-Sheet.dat"); repo1.addFile("Sales-Report.csv"); FileRepository& repo2 = FileRepository::getInstance(); std::cout << std::boolalpha << "Same object? (&repo == &repo1 ?) = " << (&repo1 == &repo2) << "\n"; std::cout << "Repository files" << std::endl; repo2.showFiles(); std::cout << "Add more files" << std::endl; repo2.addFile("fileX1.pdf"); repo2.addFile("fileX2.pdf"); repo2.addFile("fileX3.pdf"); repo2.showFiles();
Compiling and running (file:src/design-patterns/singleton1.cpp):
clang++ singleton1.cpp -o singleton1.bin -g -std=c++14 -Wall -Wextra && ./singleton1.bin [LOG] File Respository Initialized. Same object? (&repo == &repo1 ?) = true Repository files File = CashFlowStatement.txt File = Balance-Sheet.dat File = Sales-Report.csv Add more files File = CashFlowStatement.txt File = Balance-Sheet.dat File = Sales-Report.csv File = fileX1.pdf File = fileX2.pdf File = fileX3.pdf [LOG] File Respository Deleted. Ok.
1.2.3 Example 2 - OpenGL GLFW Keyboard Manager
The following singleton class allows registering multiple capturing lambda callbacks for a given keyboard key. The purpose of the class KeyboardManager is to provide an easier way to pass capturing C++11 lambdas to the function glfwSetKeyCallback(). Passing capturing lambdas to glfwSetKeyCallback() directly is not possible as this function lacks an extra void pointer for passing user data. In order to overcome this issue, a Singleton class was used as global variable for passing lambda to the GLFW callback.
Relevant type signatures:
GLFWkeyfun glfwSetKeyCallback ( GLFWwindow * window , GLFWkeyfun callback ); // C notation for function pointer type alias. typedef void(* GLFWkeyfun) (GLFWwindow *, int, int, int, int); // C++ notation for function pointer type alias. using GLFWkeyfun = void (*) (GLFWwindow *, int, int, int, int);
File: glfw_kbd_singleton.cpp
// Brief: Draw curve copying data to the GPU #include <iostream> #include <vector> #include <array> #include <functional> #include <map> #include <cmath> #include <cassert> // -------- OpenGL headers ---------// #define GL_GLEXT_PROTOTYPES 1 // Necessary for OpenGL >= 3.0 functions #define GL3_PROTOTYPES 1 // Necessary for OpenGL >= 3.0 functions #include <GL/gl.h> #include <GLFW/glfw3.h> // #include <GL/glew.h> #include <GL/glu.h> #include <GL/glut.h> // Singleton object for registering lambda event handlers for GLFW keyboard typing events. class KeyboardManager { public: using KeyCallback = std::function<void (GLFWwindow* window)>; using KeyCallbackDB = std::map<int, KeyCallback>; private: KeyCallbackDB m_keydb{}; GLFWwindow* m_window = nullptr; // Private constructor for not allowing client code creating more // than one instance. KeyboardManager() { std::cerr << " [TRACE] KeyboardManager - Singleton initialized." << "\n"; } // Forbid client code from creating a copy or using the // copy constructor. KeyboardManager(const KeyboardManager&){} public: ~KeyboardManager() = default; // This is the unique way to get singleton instance. static KeyboardManager& getInstance() { static KeyboardManager instance{}; return instance; } KeyboardManager& set_window(GLFWwindow* window) { m_window = window; // =>> Note: This lambda must be non-capturing. Otherwise, it will result // in compile-time error. // // =>> Note: All this redundancy and boilerplate code is need because // the function glfwSetKeyCallback() lacks an extra void* pointer // for passing custom user data. ::glfwSetKeyCallback(m_window ,[](GLFWwindow* window, int key, int scancode, int action, int mods) { std::cerr << " [TRACE] glfwSetKeyCallback() key = " << key << " / char " << (char) key << '\n'; // Get singletion instance KeyboardManager& instance = KeyboardManager::getInstance(); auto it = instance.m_keydb.find(key); if(it == instance.m_keydb.end()){ return; } // Call callback functions. it->second(window); }); return *this; } KeyboardManager& add_keyCallback(int key, KeyCallback callback) { this->m_keydb[key] = callback; return *this; } }; int main(int argc, char** argv) { // Initialize GLFW if ( !glfwInit() ){ return -1; } glutInit(&argc, argv); /* Create a windowed mode window and its OpenGL context */ GLFWwindow* window = glfwCreateWindow(640, 480, "Simple triangle draw", NULL, NULL); if (!window) { glfwTerminate(); return -1; } .... ... .... ... .... ... .... ... .... ... .... ... .... ... .... ... // Get singleton instance and set window. KeyBoardMager& kdb = KeyboardManager::getInstance().set_window(window); GLenum draw_type = GL_TRIANGLES; // Register events kdb.add_keyCallback('A', [&draw_type](GLFWwindow* win){ draw_type = GL_TRIANGLES; }); kdb.add_keyCallback('B', [&draw_type](GLFWwindow* win){ draw_type = GL_LINE_LOOP; }); kdb.add_keyCallback('C', [&draw_type](GLFWwindow* win){ draw_type = GL_LINES; }); kdb.add_keyCallback('D', [&draw_type](GLFWwindow* win){ draw_type = GL_LINE_STRIP; }); kdb.add_keyCallback('F', [&draw_type](GLFWwindow* win){ draw_type = GL_LINE; }); // Positive rotatation of 10 degrees around Z axis (counter - clockwise) kdb.add_keyCallback(GLFW_KEY_RIGHT, [&](GLFWwindow* win) { std::cerr << " [TRACE] Postive rotation around Z axis " << '\n'; angle = angle + 10.0; glUniform1f(uniform_angle, angle); }); ... ... ... ... ... ... ... ... ... ... ... ... // ============== R E N D E R - L O O P ===========================// // /* Loop until the user closes the window */ while (!glfwWindowShouldClose(window)) { .... .... .... .... .... .... .... .... .... .... } } // ------ End of main() ---------------------------------------------//
1.2.4 Example 3 - Generic Singleton
This code contains a reusable-generic singleton template class, which is applied to the previous case for calling lambdas from a C++ function that accepts function pointer and does not contain an extra void pointer for allowing the calling code to pass state or non-capturing lambdas.
Generic singleton templated class:
template<typename Impl> class CRTP_Singleton { protected: CRTP_Singleton() = default; public: CRTP_Singleton(const CRTP_Singleton&) = delete; CRTP_Singleton& operator=(CRTP_Singleton const&) = delete; static Impl& getInstance() { static Impl impl; return impl; } };
Singleton class that inherits CRTP_Singleton:
// Singleton object for registering lambda event handlers for GLFW keyboard typing events. class KeyboardManager: public CRTP_Singleton<KeyboardManager> { friend KeyboardManager::CRTP_Singleton<KeyboardManager>; public: using KeyCallback = std::function<void (GLFWwindow* window)>; using KeyCallbackDB = std::map<int, KeyCallback>; protected: KeyCallbackDB m_keydb{}; GLFWwindow* m_window = nullptr; // Private constructor for not allowing client code creating more // than one instance. KeyboardManager(){ } public: ~KeyboardManager() = default; KeyboardManager& set_window(GLFWwindow* window) { m_window = window; assert( m_window != nullptr ); ::glfwSetKeyCallback(m_window ,[](GLFWwindow* window, int key, int scancode, int action, int mods) { std::cerr << " [TRACE] glfwSetKeyCallback() key = " << key << " / char " << (char) key << '\n'; // Get singleton instance KeyboardManager& instance = KeyboardManager::getInstance(); auto it = instance.m_keydb.find(key); if(it == instance.m_keydb.end()){ return; } std::fprintf(stderr, " [TRACE] Execute callback function. \n"); // Call callback functions. it->second(window); }); return *this; } KeyboardManager& add_keyCallback(int key, KeyCallback callback) { std::fprintf(stderr, " [TRACE] Add callback key. \n"); this->m_keydb[key] = callback; return *this; } };
Class usage in main() function:
... ... ... ... .... .... ... ... ... ... .... .... GLFWwindow* window = glfwCreateWindow(640, 480, "Simple triangle draw", NULL, NULL); if (!window) { std::fprintf(stderr, " [ERROR] Unable to open GLFW Window. \n"); glfwTerminate(); return -1; } ... ... ... ... .... .... // Get singleton instance and set window. auto& kdb = KeyboardManager::getInstance().set_window(window); GLenum draw_type = GL_LINE_LOOP; // Register events kdb.add_keyCallback('A', [&draw_type](GLFWwindow* win){ draw_type = GL_TRIANGLES; }); // Positive rotatation of 10 degrees around Z axis (counter - clockwise) kdb.add_keyCallback(GLFW_KEY_RIGHT, [&](GLFWwindow* win) { std::cerr << " [TRACE] Postive rotation around Z axis " << '\n'; angle = angle + 10.0; glUniform1f(uniform_angle, angle); }); ... ... ... ... .... .... ... ... ... ... .... .... ... ... ... ... .... ....
1.3 Named constructor - static factory method
The named constructor or static factory design pattern uses static methods instead of constructors for instantiating objects. This approach has many advantages over constructor instantiation. Named constructors are more readable than ordinary constructors and unlike constructors, many named constructors static methods sharing the same type signature can coexist. Another benefit is that this technique allows objects to be instantiated in many different ways from several different data representation.
Side note: It should not be confused with factory design pattern or abstract factory design pattern.
Example:
#include <iostream> #include <ostream> #include <cstdint> // #include <stdint> // WARNING - It may not be available // Unsigned byte from 0 to 255 or 0x00 to 0xFF // ---> typedef uint8_t ubyte; using ubyte = uint8_t ; class Color{ private: ubyte m_r; ubyte m_g; ubyte m_b; public: Color(ubyte red, ubyte green, ubyte blue): m_r(red), m_g(green), m_b(blue) {} ubyte red(){ return m_r; } ubyte blue(){ return m_b; } ubyte green(){ return m_g; } // Named constructor or static factory method which builds the object // From the RGB tuple data representation static Color fromRGB(ubyte red, ubyte green, ubyte blue){ return Color(red, green, blue); } // Named constructor which builds Color object // from hexadecimal data representation static Color fromHex(int color){ int r = color & 0xFF; int g = (color >> 8 ) & 0xFF; int b = (color >> 16) & 0xFF; return Color(r, g, b); } // Named constructor which builds a specific color. static Color colorRED(){ return fromRGB(255, 0, 0); } static Color colorBLUE(){ return fromRGB(0, 255, 0); } static Color colorGREEN(){ return fromRGB(0, 0, 255); } friend std::ostream& operator <<(std::ostream& os, const Color& c){ os << "Color(r = " << static_cast<int>(c.m_r) << ", g = " << static_cast<int>(c.m_g) << ", b = " << static_cast<int>(c.m_b) << ")"; return os; } }; int main(){ std::cout << "Red = " << Color::colorRED() << "\n"; std::cout << "Blue = " << Color::colorBLUE() << "\n"; std::cout << "Green = " << Color::colorGREEN() << "\n"; std::cout << "Color1 = " << Color::fromRGB(20, 90, 200) << "\n"; std::cout << "Color2 = " << Color::fromHex(0xFF8AB5) << "\n"; std::cout.flush(); return 0; }
Red = Color(r = 255, g = 0, b = 0) Blue = Color(r = 0, g = 255, b = 0) Green = Color(r = 0, g = 0, b = 255) Color1 = Color(r = 20, g = 90, b = 200) Color2 = Color(r = 181, g = 138, b = 255)
See:
1.4 Factory Method
1.4.1 Overview
Factory method design pattern allows instatiating classes from a class hierarchy without hardcoding the class name and knowing the class implementation. There are many variations of this design pattern, however the most common form is a class with method which returns an instance of a derived class based on some input such as a number, code, class name as string or number.
Definitions:
- Factory method or factory functions are any methods or functions which returns an instance of a class or derived class.
Note: The factory design pattern in this document is the variation which creates an instance of a set of derived classes based on some input.
1.4.2 Factory Method Pattern Variations
Some variations:
- An ordinary function whith if-else statements which returns an
instance of a set of derived classes according to some
input.
- Disadvantage: Adding new derived classes requires code modification.
- A static method with if-else statements. This is mostly used on
languages where there are not free functions or ordinary
functions.
- Disadvantage: Requires modifying the code when new classes are added to the hierarchy.
- Registry-based factory (mostly known as Polymorphic Factory)- A
class contains a factory method for instantiating derived classes
based on some input and a hash table for registration of derived
classes.
- Disadvantage: Possibly requires manual registration during the program initialization.
- Example:
- Factory.register("sql", SQLCreator)
- Factory.register("mysql", MYSQLCreator)
- DBDrivef driver = Factory.create("sqlite");
- Reflection-base factory - Some implementations uses reflection
registration of derived classes.
- Disadvantage: May not be possible in languages without reflection such as C++.
1.4.3 Sample Class Hierarchy
Consider the following class hierarchy:
- Base class
class Base{ public: Base() = default; // Destructor of base class must always be virtual virtual ~Base() = default; virtual auto getType() const -> std::string { return "Base"; } void showType(){ std::cout << "Class type = " << this->getType() << "\n"; } };
- Derived class A
class DerivedA: public Base{ public: DerivedA(){} auto getType() const -> std::string { return "DerivedA"; } };
- Derived class B
class DerivedB: public Base{ public: DerivedB(){} auto getType() const -> std::string { return "DerivedB"; } };
1.4.4 Simple factory - if-else statement
Note: This implementation uses a factory function instead of a factory method. The function creates an instance of Base, DerivedA or DerivedB based on the provided name. The problem of this implementation is that it requires modification of the code when new derived classes are added.
#include <memory> // smart pointers auto simpleFactory(const std::string& name) -> std::unique<Base> { if(name == "Base") return std::make_unique<Base>(); if(name == "DerivedA") return std::make_unique<DerivedA>(); if(name == "DerivedB") return std::make_unique<DerivedB>(); return nullptr; } >> auto base = simpleFactory("Base"); >> if(base) { std::puts("Ok, then proceed."); } Ok, then proceed. >> base->getType() (std::string) "Base" >> auto da = simpleFactory("DerivedA"); >> da->getType() (std::string) "DerivedA" >>
1.4.5 Polymorphic Factory - registry based factory
Despite this implementation be more complex than a factory-method pattern implementation using if-else statement, this one doesn't require source modification if new derived classes are added to the hierarchy.
The class Factory is used for instantiating derived classes of Base class based on the class name provided as a string. The factory methods in this class are makeUnique and makeRaw.
- File:
- Online Compiler:
class Factory{ private: using FactoryMap = std::map<std::string, Factory*>; // Force global variable to be initialized, thus it avoid // the inialization order fisaco. static auto getRegister() -> FactoryMap& { static FactoryMap classRegister{}; return classRegister; } public: /** Register factory object of derived class */ static auto registerFactory(const std::string& name, Factory* factory) -> void { auto& reg = Factory::getRegister(); reg[name] = factory; } /** Show all registered classes */ static auto showClasses() -> void { std::cout << " Registered classes. " << "\n"; std::cout << " =================== " << "\n"; for(const auto& pair: Factory::getRegister()) std::cout << " + " << pair.first << "\n"; } /** Construct derived class returning a raw pointer */ static auto makeRaw(const std::string& name) -> Base* { auto it = Factory::getRegister().find(name); if(it != Factory::getRegister().end()) return it->second->construct(); return nullptr; } /** Construct derived class returning an unique ptr */ static auto makeUnique(const std::string& name) -> std::unique_ptr<Base>{ return std::unique_ptr<Base>(Factory::makeRaw(name)); } // Destructor virtual ~Factory() = default; virtual auto construct() const -> Base* = 0; };
Concrete Factory:
- Template class used for creating static object which registers the associated derived class during the program initialization before the function main.
template<typename DerivedClass> class ConcreteFactory: Factory{ public: // Register this global object on the Factory register ConcreteFactory(const std::string& name){ std::cerr << " [TRACE] " << " Registered Class = " << name << "\n"; Factory::registerFactory(name, this); } auto construct() const -> Base* { return new DerivedClass; } };
Manual class registration:
// Register Base class namespace { // Anonymous namespace is used to make the definitions here private to the current // compilation unit (current file). It is equivalent to the old C static keyword. // It could be placed at Base.cpp ConcreteFactory<Base> factoryBase("Base"); }
Class registration with macros:
// Macro for class registration #define REGISER_FACTORY(derivedClass) \ namespace { auto registry_ ## derivedClass = ConcreteFactory<derivedClass>(#derivedClass); } // Registration with macro. REGISER_FACTORY(DerivedA); REGISER_FACTORY(DerivedB);
Main function:
Factory::showClasses(); std::cout << "\n"; std::unique_ptr<Base> objBase = Factory::makeUnique("Base"); std::cout << " type of objBase = " << objBase->getType() << "\n"; std::unique_ptr<Base> objDA = Factory::makeUnique("DerivedA"); std::cout << " type of derivedA = " << objDA->getType() << "\n"; std::unique_ptr<Base> objDB = Factory::makeUnique("DerivedB"); std::cout << " type of derivedA = " << objDB->getType() << "\n"; std::unique_ptr<Base> objDC = Factory::makeUnique("Derived-error"); if(!objDC) std::cout << " ==> Error: object not found in factory" << '\n';
Output:
$ clang++ factory-pattern1.cpp -o factory-pattern1.bin -g -std=c++1z -Wall -Wextra $ $ ./factory-pattern1.bin [TRACE] Registered Class = Base [TRACE] Registered Class = DerivedA [TRACE] Registered Class = DerivedB Registered classes. =================== + Base + DerivedA + DerivedB type of objBase = Base type of derivedA = DerivedA type of derivedA = DerivedB ==> Error: object not found in factory
1.4.6 References
References
- Bruce Eckel - Thinking in Patterns with Java - Page: 53
- Bruce Eckel - Thinking in C++, 2nd edition, Volume 2
- http://www.lsi.us.es/~jtorres/LibroJava/TIJ2R3-17_.html
- AndyPatterns - From Strategy to Bridge
- CDI, Polymorphism and The Factory Pattern
- Design patterns - OOD Lecture 6 - https://www.it.uu.se/edu/course/homepage/ood/ht12/overview/patterns/Lecture6.pdf
- Creational Patterns for Variability
- the Factory Design Pattern - https://cs.anu.edu.au/courses/comp2500/notes/15factory.slides.pdf
- Design Patterns as Quality Influencing Factor in Object Oriented Design Approach - https://arxiv.org/pdf/1402.2372.pdf
- Factory Design Pattern in Java - JournalDev
- Introduce Polymorphic Creation with Factory Method
- Factory Method - https://www.dofactory.com/net/factory-method-design-pattern
Further Reading
- Doing something on shared library initialization
- Factory Design Pattern in C++
- Abstract Factory Step-by-Step Implementation in C++
- Sutter's Mill - GotW #90 Solution: Factories
- Unforgettable Factory Registration
- C++: Factory With Self-Registering Types
- Object Factories in a Static Library
- Automatic object factory in C++
- Self Registering Classes - Taking polymorphism to the limit
1.5 Builder (Joshua Blosh)
The purpose of the builder design pattern proposed by Joshua Bloch is to simplify the instantiation of objects with many constructor parameters or many optional parameters. Note: it should not be confused with the GOF (Gang of Four) builder pattern.
Example:
- File: builder.cpp
// Joshua Bloch's Builder Pattern for simplifying the instantiation // of objects with many constructor parameters. It is not the // GOF (Gang of Four) builder pattern. #include <iostream> #include <string> // Function meta object class UserData{ public: using ulong = unsigned long; private: ulong _userID = 0; std::string _name; std::string _lastName; std::string _email; UserData() = default; public: // Explicit is better than implicit ~UserData() = default; auto show() -> void { std::cout << "\nUser{" << "\n" << " id = " << _userID << "\n" << " name = " << _name << "\n" << " last name = " << _lastName << "\n" << " email = " << _email << "\n" << "}" << "\n"; } // Allow builder class access UserData's private data friend class UserBuilder; }; //--- EoF class UserData --- // class UserBuilder{ private: //class UserData; UserData _data{}; public: UserBuilder(){ // _data = UserData(); } auto setID(ulong userID ) -> UserBuilder& { _data._userID = userID; return *this; } auto setName(const std::string& name) -> UserBuilder& { _data._name = name; return *this; } auto setLastName(const std::string& name) -> UserBuilder& { _data._lastName = name; return *this; } auto setEmail(const std::string& email) -> UserBuilder& { _data._email = email; return *this; } auto build() -> UserData { return this->_data; } }; //--- EoF class UserData::builder --- // int main(){ auto user0 = UserBuilder() .setID(2065) .setName("John") .setLastName("Von Neumman") .setEmail("nx098774a@sknmap.co") .build(); auto user1 = UserBuilder() .setID(1065) .setName("Blaise") .setLastName("Pascal") .setEmail("dummyEmail@service1.co.uk") .build(); auto user2 = UserBuilder() .setID(2001) .setName("Nikola") .setLastName("Tesla") .setEmail("wsx752@couk.com.sk") .build(); user0.show(); user1.show(); user2.show(); return EXIT_SUCCESS; }
Running:
$ g++ builder.cpp -o builder.bin -g -std=c++1z -Wall -Wextra && ./builder.bin
User{
id = 2065
name = John
last name = Von Neumman
email = nx098774a@sknmap.co
}
User{
id = 1065
name = Blaise
last name = Pascal
email = dummyEmail@service1.co.uk
}
User{
id = 2001
name = Nikola
last name = Tesla
email = wsx752@couk.com.sk
}
1.6 Decorator
1.6.1 Overview
Structural design pattern which allows to extend a funcitonality of an object dynamically without deep inheritance or deep class hierarchies.
- Decorators are also known as wrappers due to it wrap itself around anohter object.
- The wrapping/enclosing object is called "decorator".
GOF: "Attach additional resposibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality."
Diagra built with: ascii flow
+-------------------------------------+ | <<Interface>> IComponent | |-------------------------------------| | +Operation() | +------------^----------------^-------+ | | +------+ + | +-------+ | | +----------+-----------+ +-----------------------------+ | Component | | Decorator | |----------------------| |-----------------------------| | +Operation() | | Variable: IComponen comp | | | | | +----------------------+ | +Decorator(IComopent comp) | | | | +Operation(){ | | newBehavior(); | | comp.Operation(); | | } | +-----------------------------+
1.6.2 Example
In this demonstration, it is presented a 2D stateful graphical API similar to OpenGL or Windows' GDI which draws shapes on the screen. There are two shapes Square and Triangle and two decorators. ColorDecorator which sets the current context color saving the current color and restoring it later and PositionDecorator which changes the current coordinate in the transformation matrix, draws the wrapped object and then restore the matrix to its previous state. The use of decorators allows the functionality of some shape to be extended without modifying or inheriting it. Another benefit is that decorators can be combined with each other without inheritance.
- File: file:src/design-patterns/decorator1.cpp
- Online Compiler: https://rextester.com/GSF96195
Interface IShape:
// Interface IShape (IComponent) struct IShape { virtual auto draw() -> void = 0; virtual auto description() -> std::string = 0; virtual ~IShape() = default; };
Concrete shape: Square
// Component class class Square: public IShape { public: auto draw() -> void { std::cout << " => Draw square" << std::endl; } auto description() -> std::string { return "square"; } };
Concrete shape: Triangle
// Component class class Triangle: public IShape { public: auto draw() -> void { std::cout << " => Draw tringle" << std::endl; } auto description() -> std::string { return "triangle"; } };
Decorator class for drawing a shape with some color.
// Decorator 1 => Draw shape with color class ColorDecorator: public IShape { public: ColorDecorator(std::shared_ptr<IShape> shape) : m_shape(shape) { } auto draw() -> void { // Save color: push() std::cout << " => [ColorDecorator] Draw object with color blue" << std::endl; m_shape->draw(); // Restore color: pop() } auto description() -> std::string { return m_shape->description() + " ; color = " + m_color; } // Return a reference to itself (ColorDecorator&) auto setColor(const std::string& color) -> decltype(*this)& { m_color = color; return *this; } private: // The decorator owns the decorated object std::shared_ptr<IShape> m_shape; std::string m_color = "blue"; };
Decorator class for drawing a shape at some position by saving, changing and restoring the transformation matrix.
class PositionDecorator: public IShape { public: PositionDecorator(std::shared_ptr<IShape> shape) : m_shape(shape){ } auto draw() -> void { // Save transformation matrix: pushMatrix() std::cout << " => [PositionDecorator] Draw object at x = " << m_x << " ; y = " << m_y << std::endl; m_shape->draw(); // Restore transformation matrix: popMatrix() } auto description() -> std::string { return m_shape->description() + " ; position x = " + std::to_string(m_x) + " , y = " + std::to_string(m_y); } auto setPosition(double x, double y) -> PositionDecorator& { m_x = x, m_y = y; return *this; } private: // The decorator owns the decorated object std::shared_ptr<IShape> m_shape; int m_x = 0, m_y = 0; };
Building
$ clang++ decorator1.cpp -o decorator1.bin -g -std=c++14 -Wall -Wextra
$ ./decorator1.bin
Experiment 1
std::puts("\n ======>> Experiment 1 <<==========="); auto shape = std::make_shared<ColorDecorator>(std::make_shared<Square>()); shape->setColor("yellow"); shape->draw();
Output:
======>> Experiment 1 <<=========== => [ColorDecorator] Draw object with color blue => Draw square DESCRIPTION = square ; color = yellow
Experiment 2
std::puts("\n ======>> Experiment 2 <<==========="); auto observerd = std::shared_ptr<IShape>{nullptr}; auto shapeWithColorAndPosition = [&observerd]{ auto shape = std::make_shared<Triangle>(); observerd = shape; auto shapeColored = std::make_shared<ColorDecorator>(shape); shapeColored->setColor("white"); auto shapePos = std::make_shared<PositionDecorator>(shapeColored); return shapePos; }(); shapeWithColorAndPosition->setPosition(100, 20); shapeWithColorAndPosition->draw(); std::cout << " DESCRIPTION = " << shapeWithColorAndPosition->description() << std::endl; std::cout << " [INFO] observed shape = " << observerd->description() << std::endl;
Output:
======>> Experiment 2 <<=========== => [PositionDecorator] Draw object at x = 100 ; y = 20 => [ColorDecorator] Draw object with color blue => Draw tringle DESCRIPTION = triangle ; color = white ; position x = 100 , y = 20 [INFO] observed shape = triangle
1.6.3 Other implementations
- Design Patterns & C++14
- C++11: YES
- Note: Uses smart pointer (std::unique_ptr).
- C++ - Design pattern - Decorator | BadproG.com
- C++11: YES
- Note: It uses smart pointer (std:unique_ptr) for the wrapped object. All objects in the main functions are allocated on the heap using std::make_unique (C++14).
- Decorator Design Pattern in C++: Before and after
- C++11: NO
- Note: It uses raw pointer for accessing heap-allocated wrapped objects. All member objects are allocated on the heap and deleted manually. All objects in the main function are allocated on the hep (free-store).
- Note: It doesn't follow modern C++ best practices which advise to manage dynamic memory with smart pointers.
- Decorator Pattern | Set 3 (Coding the Design) - GeeksforGeeks
- C++11: NO
- Note: Uses raw pointers, heap-allocated objects and manual delete.
- Decorator pattern in C++ - Stack Overflow
- C++11: NO
- Note: All objects allocated on the heap.
- Decorator pattern - Wikipedia
- C++11: NO
- The decorator class uses a raw pointer to the wrapped object. All objects in the main functions are allocated on the stack. In this example, there is any problem over using raw pointers since they don't own memory.
- Note: Also shows the decorator implemented with Mixins.
- Design Patterns: Decorator Pattern - 2018
- Uses raw pointer for wrapped object. All objects in main functions are allocated on the heap.
- C++ Decorator Pattern ~ Programming Tutorials by SourceTricks
1.7 Strategy
1.7.1 Overview
Intent: A behavioral design pattern from GOF which allows the client code to select and change an algorithm encapsulated as an object at runtime.
Note: this design pattern is similar to a callback and can be simplified with functional programming.
Parts:
- Context:
- Object that has a reference to an strategy objects and sets the strategy at runtime.
- Resposibilities:
- Set strategy
- Change strategy
- Invoke strategy
- IStrategy: (Algorithm interface)
- Strategt interface define the algorithm operations.
- Concrete strategy.
- Strategies objects or implementation of IStrategy class.
1.7.2 Example
Code:
- File: file:src/design-patterns/strategy-pattern1.cpp
- Online Compiler: https://rextester.com/WQO93455
Strategy interface:
// Strategy interface struct IStrategy{ virtual ~IStrategy(){} // Essential: Algorithm encapsulated by strategy object virtual auto compute(double x, double y) const -> double = 0; // Optional: Provides strategy metadata virtual auto name() const -> const std::string = 0; // Clone this object (Note: This is a virtual constructor) virtual auto clone() const -> IStrategy* = 0; };
Context Class:
- Selects and switch the strategy (aka algorithm).
class Context{ private: std::unique_ptr<IStrategy> _strategy; public: Context() : _strategy{nullptr} { } Context(IStrategy* s) : _strategy{s} { } Context(const IStrategy& s) : _strategy{s.clone()} { } auto setStrategy(IStrategy* s){ _strategy.reset(s); } auto setStrategy(const IStrategy& s){ _strategy.reset(s.clone()); } auto compute(double x, double y) -> void { if(_strategy == nullptr) std::runtime_error("Error: strategy not set"); double result = _strategy->compute(x, y); std::cout << " strategy = " << _strategy->name() << " " << "( x = " << x << " ; " << "y = " << y << " )" << "\n" ; std::cout << "Result = " << result << "\n"; } };
Concrete strategies:
- add => algorithm which adds two numbers.
class AddStrategy: public IStrategy { public: auto name() const -> const std::string{ return "add"; } auto compute(double x, double y) const -> double { return x + y; } auto clone() const -> IStrategy* { std::cerr << " [TRACE] AddStrategy => I was cloned" << "\n"; return new AddStrategy(*this); } };
- Multiplication
struct MulStrategy: public IStrategy { public: auto name() const -> const std::string{ return "mul"; } double compute(double x, double y) const { return x + y; } auto clone() const -> IStrategy* { std::cerr << " [TRACE] MulStrategy => I was cloned" << "\n"; return new MulStrategy(*this); } };
- Linear Combination:
struct LinearCombStrategy: public IStrategy { double a, b, c; LinearCombStrategy(double a, double b, double c) : a(a), b(b), c(c) { } auto name() const -> const std::string{ return "Linear combination a * x + b * y + c"; } auto compute(double x, double y) const -> double{ return a * x + b * y + c; } auto clone() const -> IStrategy* { std::cerr << " [TRACE] LinearCombStrategy => I was cloned" << "\n"; return new LinearCombStrategy(*this); } };
Main function:
Context ctx; std::cout << "==== Strategy = add ====" << "\n"; ctx.setStrategy(new AddStrategy); ctx.compute(3.0, 4.0); std::cout << "==== Strategy = mul ====" << "\n"; ctx.setStrategy(new MulStrategy); ctx.compute(3.0, 4.0); std::cout << "==== Strategy = Linear combination ====" << "\n"; ctx.setStrategy(new LinearCombStrategy(5, 3, 4)); ctx.compute(3.0, 4.0); std::cout << "==== Strategy = Linear combination [2] ====" << "\n"; auto comb1 = LinearCombStrategy(6.0, 5.0, 10.0); // Copy stack-allocated object comb1 using the virtual constructor ctx.setStrategy(comb1); ctx.compute(5.0, 3.0); std::cout << "==== Strategy = Linear combination [2] ====" << "\n"; // Copy stack-allocated temporary object comb1 using the virtual constructor // clone ctx.setStrategy(LinearCombStrategy{6.0, 5.0, 10.0}); ctx.compute(2.0, 6.0);
Output:
$ clang++ strategy-pattern1.cpp -o strategy-pattern1.bin -g -std=c++1z -Wall -Wextra $ ./strategy-pattern1.bin ==== Strategy = add ==== strategy = add ( x = 3 ; y = 4 ) Result = 7 ==== Strategy = mul ==== strategy = mul ( x = 3 ; y = 4 ) Result = 7 ==== Strategy = Linear combination ==== strategy = Linear combination a * x + b * y + c ( x = 3 ; y = 4 ) Result = 31 ==== Strategy = Linear combination [2] ==== [TRACE] LinearCombStrategy => I was cloned strategy = Linear combination a * x + b * y + c ( x = 5 ; y = 3 ) Result = 55 ==== Strategy = Linear combination [2] ==== [TRACE] LinearCombStrategy => I was cloned strategy = Linear combination a * x + b * y + c ( x = 2 ; y = 6 ) Result = 52
1.8 Method Chaining - Fluent API
class CharacterSuperMutant{ private: double m_x; double m_y; double m_z; public: CharacterSuperMutant& setX(double x){ m_x = x; return *this; } CharacterSuperMutant& setPosition(double x, double y, double z); auto setColor(COLOR color) -> decltype(this*)& { ... .... return *this; } // C++ 11 auto setForce(double force) -> CharacterSuperMutant& { ... ... return *this; } // Method / member function declaration, the implemention is in a different file. CharacterSuperMutant& CharacterSuperMutant::setPosition(double x, double y, double z); // C++11 auto CharacterSuperMutant::setStamina(double x, double y, double z) -> CharacterSuperMutant&; }; // Example: Method implemented separated from class declaration in .cpp file. CharacterSuperMutant& CharacterSuperMutant::setPosition(double x, double y, double z){ m_x = x; m_y = y; m_z = z; return *this; } // C++11 auto syntax auto CharacterSuperMutant::setStamina(double x, double y, double z) -> CharacterSuperMutant& { // ... ... ... return *this; }
Usage:
CharacterSuperMutant mutant1; mutant1.setForce(1000).setColor(BLUE).setPosition(x).show(); // Set methods at initialization auto superMonster = CharacterSuperMutant().setForce(100).setPosition(4300).; // Instead of: mutant1.setForce(1000); mutant1.setColor(BLUE); mutant1.setPosition(x);
1.9 GOF - Template Pattern
It is stated by GOF as: "Defines the skeleton of an algorithm in a method, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithms structure."
- Intent: Create an algorithm template that allows redefine some steps without changing its structure.
The parent abstract class has four different types of methods:
- Concrete methods: Methods implemented in the abstract class.
- Abstract methods: Methods without implementation that must be implemented by subclasses.
- Hook methods: Methods with default implementation that can be overriden by subclasses.
- Template methods: Method that calls concrete methods, abstract methods or hook methods.
Participants:
- Base Class: Defines an algorithm which calls primitive methods (aka hook methods) that will be defined by the derived classes.
- Derived Class: Implements primitive methods (virtual methods or methods that can be overriden) defined in the base class.
Features:
- The base class defines an algorithm stub, however some steps are required to be implemented by the derived class.
- This pattern provides an inverse control structure, the algorithm defined in the base class which calls the primitive methods which are algorithm steps declared in the base class.
- This pattern can be the base of an framework.
Example:
class IntervalSummation{ public: // Algorithm or entry point which calls the derived class method. // This is the template method double summation(int lower, int upper) const{ double sum = 0; for(int i = lower; i <= upper; i++) sum += this->stepFn(i); return sum; } protected: // Hook method or to be defined by the derived class virtual double stepFn(double x) const = 0 ; }; class SumOfSquares: public IntervalSummation{ private: double stepFn(double x) const { return x * x; } }; class SumOfCubes: public IntervalSummation{ private: double stepFn(double x) const { return x * x * x; } }; void clientCode(const IntervalSummation& obj){ std::cout << "Summation at [0, 15] = " << obj.summation(0, 15) << std::endl; }
Running in CERN's ROOT REPL:
>> sq.summation(0, 10) (double) 385.00000 >> sc.summation(0, 10) (double) 3025.0000 >> clientCode(sq) Summation at [0, 15] = 1240 >> clientCode(sc) Summation at [0, 15] = 14400 >>
References:
- Template Method, Factory Method, and Composite. - http://condor.depaul.edu/cjones1/depaul/se455/notes/lecture07.pdf
- Template Method - http://cs.unb.ca/~wdu/cs4015/ch5k.pdf
- Encapsulating Algorithms with the Template Method Design Pattern - https://redlich.net/pdf/publications/jupitermedia/template.pdf
- Template Method Design Pattern in Java - https://www.journaldev.com/1763/template-method-design-pattern-in-java
1.10 Null-Object Pattern
An object, called nulll object, which doesn't do anything and has empty methods implementing a required interface by the client code is used to convey the absense of an ordinary object instead of null or null pointer.
Example: in a database system, instead of returning null, null reference or null pointer for an not found employee object, an empty object, called null object, with an empty mehtods is returned. It has the advantage of avoiding null exception that can crash unexpectdely a program. Another problem of null or null pointers is that null bugs cannot be caught at compile-time and are also known to be hard to debug and trace.
Alternatives:
- Throw an exception when there is the absense of an object. For instance, std::runtime_error("Error: record not found.").
- Use optional type (Haskell's maybe) or C++17 Optional.
- Return a pointer that is set to null for denoting the absence of an object. This pattern is widely used by many C and C++ codes. However it is prone for the infamous null pointer exception problems.
A Null Object provides a surrogate for another object that shares the same interface but does nothing. Thus, the Null Object encapsulates the implementation decisions of how to do nothing and hides those details from its collaborators. – Bobby Woolf in [PLoP3]
Code Example:
// Interface class ICompany{ virtual unsigned getID() const = 0; virtual std::string getName() const = 0; virtual void showCompany() const = 0; virtual ~ICompany() = default; }; class Company: public ICompany{ public: Company(unsigned id, const std::string& name): _name(name), _id(id){ } unsigned getID() const { return _id; } std::string getName() const { return _name; } void showCompany() const { std::cout << "Company is = " << _name << "\n"; } ~Company() = default; private: std::string _name; unsigned _id; }; // Null object // Returns this null object instead of returning a null pointer // when a givne company is not found in the database system. class NullCompany: public ICompany{ public: unsigned getID() const { return 0; } std::string getName() const { return ""; } void showCompany() const {} };
1.11 Composite
The composite pattern is a structural design pattern intruduced by GOF which allows clients to deal with tree collection of objects in the same way as a primitive objects.
Features and applications:
- Allow client code to ignore differences between individual objects and collection (composite objects) and treat them uniformly.
- Known cases:
- GUI Widgets - Java Swing GUI library allows to build a complex GUI out of individual UI elements such as buttons, menu, text box and panels which can have many UI child objects and other panels.
- AST - Abstract Syntax Trees
- XML - Nodes
- Participants of Composite
- Component:
- Overview: Interface or abstract class which defines common operations shared by the primitive objects and composite objects.
- Leaf
- Overview:
- Represents a primitive object which doesn't contain any child object.
- Overview:
- Composite
- Overview
- As the name implies, the composite object contains leaf children objects.
- Implements child operations in the component interface.
- Can add or remove leaf objects or composite object as children.
- The composite object forwards the messages related to child operations to the children elements.
- Methods:
- .Add(Component)
- .Remove(Component)
- .Clear()
- Overview
- Component:
Example:
File:
Online Compiler:
Component interface:
- Defines primitive elements operations.
class IGraphic{ public: virtual auto type() const -> const std::string = 0; virtual auto draw() -> void = 0; virtual auto rotate(double) -> void = 0; virtual ~IGraphic() = default; };
Convenient type aliases:
using cstring = const char*; using GNode = std::shared_ptr<IGraphic>;
Leaf, group aka composite object:
- Allows to performing an operation, which would be performed on a single primitive element, on all elements stored in the composite object.
class Group: public IGraphic{ private: std::vector<GNode> _nodes; const std::string _id; static constexpr cstring _type = "Group"; public: Group(const std::string& id): _id(id) { std::cout << " [TRACE] Create group = " << id << "\n"; } ~Group(){ std::cout << " [TRACE] Destroy group - id = << " << _id << "\n"; } auto begin() const -> decltype(_nodes.begin()) { return _nodes.begin(); } auto end() const -> decltype(_nodes.end()) { return _nodes.end(); } auto clear() -> void { _nodes.clear(); } auto size() -> size_t { return _nodes.size(); } auto add(GNode n) -> void { std::cout << " [TRACE] id = " << _id << "; Add object = " << n->type() << "\n"; _nodes.push_back(n); } auto add(IGraphic* n) -> void { std::cout << " [TRACE] id = " << _id << " ; Add object = " << n->type() << "\n"; _nodes.push_back(std::shared_ptr<IGraphic>(n)); } // Add stack-allocated object auto addFromStack(IGraphic* n) -> void { std::cout << " [TRACE] id = " << _id << " ; Add object = " << n->type() << "\n"; // Dummy deleter to avoid core dump by avoiding deleting // stack-allocated object or non-owned pointer. auto s = std::shared_ptr<IGraphic>(n, [](IGraphic*){ return ; }); _nodes.push_back(s); } // Note: Template methods should always be in the header files template<class Node> auto addNew(const std::string& id) -> void { auto n = std::make_unique<Node>(id); std::cout << " [TRACE] id = " << _id << " ; Add object = " << n->type() << "\n"; _nodes.push_back(std::move(n)); } // ======> Interface IGraphic <<=========// auto type() const -> const std::string override { return _type; } auto draw() -> void override { std::cout << " [TRACE] Draw group - id = " << _id << "\n"; for(const auto& obj: _nodes) obj->draw(); } auto rotate(double angle) -> void override { std::cout << " [TRACE] Rotate group - id = " << _id << "\n"; for(const auto& obj: _nodes) obj->rotate(angle); } };
Primitive element or node: Line
class Line: public IGraphic { private: static constexpr cstring _type = "Line"; std::string _id; public: Line(const std::string& id): _id{id} { } auto type() const -> const std::string override { return _type; } auto draw() -> void override { std::cout << " [TRACE] Draw line - id = " << _id << "\n"; } auto rotate(double angle) -> void override { std::cout << " [TRACE] Rotate line ; id = " << _id << "; angle = " << angle << "\n"; } };
Primitive element or node: Triangle
class Triangle: public IGraphic { private: static constexpr cstring _type = "Triangle"; std::string _id; public: Triangle(const std::string& id): _id{id} { } auto type() const -> const std::string override { return _type; } auto draw() -> void override { std::cout << " [TRACE] Draw triangle - id = " << _id << "\n"; } auto rotate(double angle) -> void override { std::cout << " [TRACE] Rotate triangle" << " id = " << _id << " angle = " << angle << "\n"; } };
Sample client code:
// Count total number of elements auto countElements(const Group& group) -> size_t { size_t n = 0; for(const auto& g: group){ if(g->type() == "Group"){ auto p = static_cast<Group*>(g.get()); n += countElements(*p); } else{ ++n; } } return n; }
Main function:
const char nl = '\n'; std::cout << "=== Objects construction === " << nl; auto groupA = Group("groupA"); groupA.add(new Triangle("triangleA1")); groupA.add(new Line("lineA1")); groupA.addNew<Line>("LineA2"); auto groupB = std::make_shared<Group>("GroupB"); groupB->add(new Triangle("triangleB1")); groupB->addNew<Triangle>("triangleB2"); groupB->addNew<Line>("LineB1"); groupB->addNew<Line>("LineB2"); auto triangleB3 = Triangle("triangleB3"); groupB->addFromStack(&triangleB3); groupA.add(groupB); std::cout << nl << "=== End of object construction === " << nl; std::cout << "Total of elements of groupA = " << countElements(groupA) << "\n"; std::cout << "Total of elements of groupB = " << countElements(*groupB) << "\n"; std::cout << nl << " [*] ==> Draw group B" << "\n"; groupB->draw(); std::cout << nl << " [*] ==> Rotate group B" << "\n"; groupB->rotate(90); std::cout << nl << " [*] ==> Draw group A" << "\n"; groupA.draw(); std::cout << nl << " [*] ==> Rotate group A" << "\n"; groupA.rotate(15); std::cout << nl << " [*] ==> Remove objects from group B" << "\n"; groupB->clear(); groupA.draw(); std::cout << "=== End of Program ====" << "\n";
Running: (file:src/design-patterns/composite1.cpp)
$ g++ composite1.cpp -o composite1.bin -g -std=c++1z -Wall -Wextra $ ./composite1.bin === Objects construction === [TRACE] Create group = groupA [TRACE] id = groupA ; Add object = Triangle [TRACE] id = groupA ; Add object = Line [TRACE] id = groupA ; Add object = Line [TRACE] Create group = GroupB [TRACE] id = GroupB ; Add object = Triangle [TRACE] id = GroupB ; Add object = Triangle [TRACE] id = GroupB ; Add object = Line [TRACE] id = GroupB ; Add object = Line [TRACE] id = GroupB ; Add object = Triangle [TRACE] id = groupA; Add object = Group === End of object construction === Total of elements of groupA = 8 Total of elements of groupB = 5 [*] ==> Draw group B [TRACE] Draw group - id = GroupB [TRACE] Draw triangle - id = triangleB1 [TRACE] Draw triangle - id = triangleB2 [TRACE] Draw line - id = LineB1 [TRACE] Draw line - id = LineB2 [TRACE] Draw triangle - id = triangleB3 [*] ==> Rotate group B [TRACE] Rotate group - id = GroupB [TRACE] Rotate triangle id = triangleB1 angle = 90 [TRACE] Rotate triangle id = triangleB2 angle = 90 [TRACE] Rotate line ; id = LineB1; angle = 90 [TRACE] Rotate line ; id = LineB2; angle = 90 [TRACE] Rotate triangle id = triangleB3 angle = 90 [*] ==> Draw group A [TRACE] Draw group - id = groupA [TRACE] Draw triangle - id = triangleA1 [TRACE] Draw line - id = lineA1 [TRACE] Draw line - id = LineA2 [TRACE] Draw group - id = GroupB [TRACE] Draw triangle - id = triangleB1 [TRACE] Draw triangle - id = triangleB2 [TRACE] Draw line - id = LineB1 [TRACE] Draw line - id = LineB2 [TRACE] Draw triangle - id = triangleB3 [*] ==> Rotate group A [TRACE] Rotate group - id = groupA [TRACE] Rotate triangle id = triangleA1 angle = 15 [TRACE] Rotate line ; id = lineA1; angle = 15 [TRACE] Rotate line ; id = LineA2; angle = 15 [TRACE] Rotate group - id = GroupB [TRACE] Rotate triangle id = triangleB1 angle = 15 [TRACE] Rotate triangle id = triangleB2 angle = 15 [TRACE] Rotate line ; id = LineB1; angle = 15 [TRACE] Rotate line ; id = LineB2; angle = 15 [TRACE] Rotate triangle id = triangleB3 angle = 15 [*] ==> Remove objects from group B [TRACE] Draw group - id = groupA [TRACE] Draw triangle - id = triangleA1 [TRACE] Draw line - id = lineA1 [TRACE] Draw line - id = LineA2 [TRACE] Draw group - id = GroupB === End of Program ==== [TRACE] Destroy group - id = << groupA [TRACE] Destroy group - id = << GroupB
1.12 Observer-Observable
1.12.1 Overview
The observer design pattern is a behavioral design pattern introduced by GOF(Gang-of-Four book), which defines a one-to-many dependency between a subject (a.k.a observable) and observers that are notified by the subject object whenever its state change.
This pattern is widely used in GUI graphical user interfaces for decoupling the application state and business logic from the graphical user interface. For instance, many GUI applications that provide Comboboxes, Listboxes or selection boxes widgets does not allow adding elements to those widgets directly. Instead, they provide observable objects, such as ListModel which notifies the subscribed, or attached widgets, about its changes. When any new element is added to a hypothetical ListModel object, it notifies the dependent widgets such as ListBoxes, Comboboxes and tables which update themselves. The benefit of this approach is that it allows changing the user interface widgets without modifying the application logic.
Also known as:
- observer design pattern
- publisher/subscriber
- subject/observer
Use-cases and potential applications:
- GUI - Graphical User Interfaces
- Chart Applications
- CAD - Computer Aided Design Software
- Spreadsheets
- Object-oriented callbacks
- Event-driven systems
- "Reactive programming" buzzword (ReactiveX)
Other Variants:
- MVC - Model View Controller design pattern for GUIs
- MVP - Model View Presenter design pattern for GUIs
- "Reactive programming"
- Two-way data bindings from .NET WPF (Windows Presentation Foundation)
- Observable property from .NET
- Observable variables from Java and Android
- Signals and slots from Qt Framework
Known uses:
- Java: java.util.Observer
- .NET (C#): System.IObservable and System.IObserver
- Java Swing
- QT GUI C++ Framework
Parts:
- Subject (aka Observable) - Keeps references to all observer
objects and notifies them whenever its state changes.
- Typical Methods:
- Subject.subscribe(Observer) => Register observer for further notifications.
- Subject.register(Observer) => Register observer for further notifications.
- Subject.attach(Observer) => Register observer for further notifications.
- Subject.detach(Observer) => Remove observer, unsubscribe from notifications.
- Subject.notify() => Notify all observers by calling the method .notify()
- Typical Methods:
- Observer - Object that subscribes to the subject notifications and
are called by the subject whenever there is a state change.
- Typical methods:
- .update()
- .notify()
- Typical methods:
Potential problems:
- When notified, an observer object should complete its computation as fast as possible, otherwise it will block the remaining observers from being notified.
- If the computation performed by any observer, when a notification arrives, blocks the current thread, all other remaining observers will not be able to receive notifications from the subject object.
- Possible callback hell. Every observer is just a object-oriented callback.
Mechanisms to send data to observers:
- Pull model. Observer executes Subject.getData() to get data from subject.
- Push model. The subject pass its data as argument of observers'
update method.
for o in observers do o.update(subject.data)
See also
General:
- Observer pattern - Wikipedia
- model view controller - Difference between Observer, Pub/Sub, and Data Binding - Stack Overflow
- CS 326 - Dependency and Decoupling
- McGill - Observer Design Pattern
- Exploring the Observer Design Pattern | Microsoft Docs
- Microsft - Observer Design Pattern
- http://ima.udg.edu/~sellares/EINF-ES1/ObserverToni.pdf
- Software Architecture - Lecture 7: Patterns, Observer, MVC - Betrand Meyer
- Applying Observer Pattern in C++ Applications - CodeProject
- Propagator in C# - An Alternative to the Observer Design Pattern - CodeProject
- Deprecating the Observer Pattern - EPFL Report (Paper)
Observer Pattern Variants.
- MVP: Model-View-Presenter - The Taligent Programming Model for C++ and Java
- Signal/Slot design pattern — signalslot 0.1.1 documentation
- Signals & Slots | Qt Core 5.15.0
- Chapter 35. Boost.Signals2 - 1.74.0 (Boost library)
- ReactiveX - "The Observer pattern done right. ReactiveX is a combination of the best ideas from the Observer pattern, the Iterator pattern, and functional programming"
- An easier way to manage INotifyPropertyChanged - Ayende @ Rahien
- Implement Observer Pattern in .NET (3 Techniques) - CodeProject
- A Chained Property Observer - CodeProject
- .NET: An Introduction to Delegates | Microsoft Docs
- .NET - System.IObserver<T>
- .NET - System.IObservable<T>
- ObservableCollection<T> Class (System.Collections.ObjectModel) | Microsoft Docs
- ObservableList | Android Developers
- Work with observable data objects | Android Developers
- Observable.OnPropertyChangedCallback | Android Developers
- Understanding Java Observable and JavaFX Observable - Developer.com
- JavaFX Collection's ObservableList and ObservableMap - DZone
1.12.2 Implementation 1 - Classical Implementation
This implementation is based on classical observer-design pattern and defines two interfaces IObservable, which all observable objects inherit from, and IObserver, which all observer objects inherit from. This sample application defines a counter observable object that notifies all observers when its state changes. The application also provides three observers, a console observer which prints the console (terminal) and two Qt5 Gui observers which displays that counter value.
File: CMakeLists.txt
cmake_minimum_required(VERSION 3.9) project(observer-design-pattern1) #====== Global Configurations ==================# set(CMAKE_CXX_STANDARD 17) set(CMAKE_VERBOSE_MAKEFILE ON) set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(Qt5 COMPONENTS Core Widgets REQUIRED) #=============== Target Configurations ============# add_executable( observer1 observer-pattern1.cpp ) target_link_libraries( observer1 Qt5::Core Qt5::Gui Qt5::Widgets)
File: observer-pattern1.cpp
#include <iostream> #include <iomanip> #include <QtWidgets> #include <QApplication> #include <QSysInfo> struct IObserver; struct IObservable { /** Subscribe to observable notifications */ virtual void addObserver(IObserver* obs) = 0; /** Notify all observers */ virtual void notify() = 0; virtual ~IObservable() = default; }; struct IObserver { virtual void update(IObservable* sender) = 0; virtual ~IObserver() = default; }; class BasicObservable: public IObservable { std::vector<IObserver*> m_observers{}; public: void addObserver(IObserver* obs) override { m_observers.push_back(obs); obs->update(this); } /** Notify all observers */ void notify() override { for(const auto obs: m_observers){ obs->update(this); } } }; class CounterModel: public BasicObservable { int m_counter = 0; public: void increment() { m_counter += 1; this->notify(); } void decrement() { m_counter -= 1; this->notify(); } void reset() { m_counter = 0; this->notify(); } void set(int n) { m_counter = n; this->notify(); } int get() const { return m_counter; } }; /** Concrete observer that prints the subject state in the console (terminal) */ class ConsoleView: public IObserver { /* override: IObserver::update() */ void update(IObservable* sender) override { /* Note: It can result in undefined behavior. */ int cnt = static_cast<CounterModel*>(sender)->get(); std::cout << " [CONSOLE] Counter state changed to = " << cnt << '\n'; } }; class FormView: public IObserver { QWidget m_win{}; QFormLayout* m_form; QPushButton* m_btn_increment; QPushButton* m_btn_decrement; QLabel* m_label; CounterModel& m_model; public: FormView(CounterModel& model): m_model(model) { m_win.setWindowTitle("Form View"); m_form = new QFormLayout(&m_win); m_btn_increment = new QPushButton("[+]"); m_btn_decrement = new QPushButton("[-]"); m_label = new QLabel("0"); m_form->addRow("Increment", m_btn_increment); m_form->addRow("Decrement", m_btn_decrement); m_form->addRow("Counter value", m_label); m_win.show(); /** ------ Setup event handlers ------------------- **/ QObject::connect(m_btn_decrement, &QPushButton::clicked, [self = this]{ self->m_model.decrement(); }); QObject::connect(m_btn_increment, &QPushButton::clicked, [this](){ m_model.increment(); }); } /* override: IObserver::update() */ void update(IObservable* sender) override { int cnt = static_cast<CounterModel*>(sender)->get(); m_label->setText(QString::number(cnt)); std::cout << " [QT GUI] Counter state changed to = " << cnt << '\n'; } }; class LabelView: public IObserver { QLabel m_label; public: LabelView() { m_label.setWindowTitle("Label View"); m_label.resize(400, 300); m_label.show(); } /* override: IObserver::update() */ void update(IObservable* sender) override { int cnt = static_cast<CounterModel*>(sender)->get(); m_label.setText(" [LABEL Observer] Counter value = " + QString::number(cnt)); } }; int main(int argc, char** argv) { // Useful for debugging std::cout << " [INFO] QTVersion = " << ::qVersion() << std::endl; QApplication app(argc, argv); CounterModel model; ConsoleView observerA{}; FormView observerB{model}; LabelView observerC{}; model.addObserver(&observerA); model.addObserver(&observerB); model.addObserver(&observerC); return app.exec(); }
Building and running:
# Get sources $ git clone https://gist.github.com/9331193ae1f9b05e4949160a4e024e84 observer1 && cd observer1 # Build $ cmake -H. -B_build -DCMAKE_BUILD_TYPE=Debug && cmake --build _build --target # Run: $ _build/observer1b
GUI Screenshot:
1.12.3 Implementation 2 - Using Callbacks
In this implementation, the observer object is replaced by a std::function<> callback which makes the code more loose coupled as any objects from any class and lambdas can be used as observers.
GIST:
File: CMakeLists.txt
cmake_minimum_required(VERSION 3.9) project(observer-design-pattern2) #====== Global Configurations ==================# set(CMAKE_CXX_STANDARD 17) set(CMAKE_VERBOSE_MAKEFILE ON) set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(Qt5 COMPONENTS Core Widgets REQUIRED) #=============== Target Configurations ============# add_executable( observer2 observer-pattern2.cpp ) target_link_libraries( observer2 Qt5::Core Qt5::Gui Qt5::Widgets)
File: observer-pattern2.cpp
#include <iostream> #include <iomanip> #include <functional> #include <QtWidgets> #include <QApplication> #include <QSysInfo> using ObserverCallback = std::function<void (void* sender)>; struct IObservable { /** Subscribe to observable notifications */ virtual void addObserver(ObserverCallback obs) = 0; /** Notify all observers */ virtual void notify() = 0; virtual ~IObservable() = default; }; class Observable: public IObservable { void* m_sender = nullptr; std::vector<ObserverCallback> m_observers{}; public: Observable(){ } Observable(void* sender): m_sender(sender){ } void addObserver(ObserverCallback callback) override { m_observers.push_back(callback); callback(m_sender); } /** Notify all observers */ void notify() override { for(const auto callback: m_observers){ callback(m_sender); } } }; class CounterModel { int m_counter{0}; Observable m_obs{this}; public: void increment() { m_counter += 1; m_obs.notify(); } void decrement() { m_counter -= 1; m_obs.notify(); } int get() const { return m_counter; } void onCounterChanged(ObserverCallback callback) { m_obs.addObserver(callback); } }; class FormView { QWidget m_win{}; QFormLayout* m_form; QPushButton* m_btn_increment; QPushButton* m_btn_decrement; QLabel* m_label; CounterModel& m_model; public: FormView(CounterModel& model): m_model(model) { m_win.setWindowTitle("Form View"); m_form = new QFormLayout(&m_win); m_btn_increment = new QPushButton("[+]"); m_btn_decrement = new QPushButton("[-]"); m_label = new QLabel("0"); m_form->addRow("Increment", m_btn_increment); m_form->addRow("Decrement", m_btn_decrement); m_form->addRow("Counter value", m_label); m_win.show(); /** ------ Setup event handlers ------------------- **/ QObject::connect(m_btn_decrement, &QPushButton::clicked, [self = this]{ self->m_model.decrement(); }); QObject::connect(m_btn_increment, &QPushButton::clicked, [this](){ m_model.increment(); }); } void update(void* sender) { int cnt = reinterpret_cast<CounterModel*>(sender)->get(); m_label->setText(QString::number(cnt)); std::cout << " [QT GUI] Counter state changed to = " << cnt << '\n'; } }; class LabelView { QLabel m_label; public: LabelView() { m_label.setWindowTitle("Label View"); m_label.resize(400, 300); m_label.show(); } void notify(void* sender) { int cnt = reinterpret_cast<CounterModel*>(sender)->get(); m_label.setText(" [LABEL Observer] Counter value = " + QString::number(cnt)); } }; int main(int argc, char** argv) { QApplication app(argc, argv); CounterModel model; model.onCounterChanged([](void* sender){ int cnt = static_cast<CounterModel*>(sender)->get(); std::cout << " [CONSOLE VIEW] Counter state changed to = " << cnt << '\n'; }); FormView observerB{model}; LabelView observerC{}; model.onCounterChanged([&](void* sender){ observerB.update(sender); }); model.onCounterChanged([&](void* sender){ observerC.notify(sender); }); return app.exec(); }
1.13 Chain of Responsibility
1.13.1 Overview
The Chain-of-responsibility design pattern is a behavioral design pattern, introduced by the GOF book, which allows decoupling the sender of a request object from the objects that handle the request. In this pattern, request objects, also called commands or events, are passed through a chain of handler objects that process a request or forward it to the next handler in the chain without the sender know which handler will process the request.
- Note: GOF means (Gang-of-Four Book) - Elements of Reusable Object-Oriented Software - Eric Gamma et al.
- Note: The request is also referred by some sources as command or event.
Possible use-cases:
- Request processing
- Event handling
- Error handling
- Data validation
- Replace nested if-else statements allowing this piece code to be extended without modifying it. (Open-close - OOP - Object-Oriented-Programming principle from SOLID).
Intent, according to GOF book - Elements of Reusable Object-Oriented Software:
Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it.
Further Reading:
- Chain of responsiblity design pattern - Wikipedia
- Chain of responsiblity design pattern in Java - Code Nuclear
- Chain of responsiblity - An Elegant Way ot handle complex validation
- Chain of Responsility as catamorphisms (Mark Seemann)
- ABAP with Coffee and Chain of Responsibility(CoR) OO Pattern - SAP company (Sreejith Ramachandran)
- Using Chain Of Responsibility Instead of if/else Statement (Fitim Skenderi)
1.13.2 Http Request handling - Implementation 1
The following code simulates a http server that receives a http requests and sends a response which depends on the http request. This hypothetical http server has the following routes:
- '/' or '' for
http://www.mysite.com/
(GET http method) - '/download' for
http://www.mysite.com/download
(GET http method) - '/upload' for
http://www.mysite.com/upload
(POST http method) - Any other path (aka resource) that does not exist. For
http://www.mysite.com/do-not-exist
(GET http method) => Returns 404 not found http response.
All this code is available at GIST:
File: chain-of-responsibility1.cpp
#include <iostream> #include <string> enum class HttpMethod { GET, POST, DELETE, PUT, HEAD, PATCH, UKNOWN_METHOD }; struct HttpRequest { std::string path = "/"; HttpMethod method = HttpMethod::GET; std::string payload = ""; // Missing - http headers ---- // bool isGET() const { return method == HttpMethod::GET; } bool isPOST() const { return method == HttpMethod::POST; } bool isDELETE() const { return method == HttpMethod::DELETE; } }; /** Process server Http GET request and returns a response. */ void dispatch_request(HttpRequest const& req) { // Http route: http://www.mysite.com/ if(req.isGET() && ( req.path == "/" || req.path == "")) { std::printf(" => Serve index page, web site homepage \n"); // Http route: http://www.mysite.com/download } else if( req.isGET() && req.path == "/download" ) { std::printf(" => Serve http://www.mysite.com/dowload page. "); // Http route: http://www.mysite.com/upload } else if( req.isPOST() && req.path == "/upload") { std::printf(" => Upload document to web server \n"); // Http route: http://www.mysite.com/<error-failure> } else { std::printf(" => Error: 404 - Not Found resource: '%s' \n", req.path.c_str()); } } int main(int argc, char** argv) { auto req1 = HttpRequest{ "/", HttpMethod::GET, "" }; dispatch_request(req1); auto req2 = HttpRequest{ "/error/resource", HttpMethod::GET, "" }; dispatch_request(req2); auto req3 = HttpRequest{ "/upload", HttpMethod::POST, "file data" }; dispatch_request(req3); return 0; }
Compiling:
$ g++ chain-of-responsibility1.cpp -o cor1 -std=c++1z -Wall -Wextra $ ./cor1 => Serve index page, web site homepage => Error: 404 - Not Found resource: '/error/resource' => Upload document to web server
Some drawbacks of the previous code are: adding new http routes or resources, requires modification of the dispatch_request() function which violates the open-close principle of object-oriented programming which states that the code should be open for extension but closed to modification.
1.13.3 Http Request handling - Implementation 2 (old c++ style)
This implementation of the chain-of-responsibility design pattern is similar to the one presented by the GOF book. A handler object checks whether the request can be handled, otherwise the handler forwards the request to next handler in the chain. In this particular case, the request is a http request and the handler is an object that checks if it can serve the http request by sending a http response back to the client side (request sender). Otherwise, the current handler forwards the request to the next http handler in the chain until the last handler is reached.
The advantage of this implementation using the chain-of-responsibility design pattern over the previous one using nested if else is that new http handlers (http routes or http resources) can be added without changing the server code.
Note: This implementation was designed in old C++ style (C++98) without using smart pointers.
File: chain-of-responbility2.cpp
#include <iostream> #include <string> enum class HttpMethod { GET, POST, DELETE, PUT, HEAD }; /** Request object - encapsulates a http request */ struct HttpRequest { std::string path = "/"; HttpMethod method = HttpMethod::GET; std::string payload = ""; // Missing - http headers implementation ---- // bool isGET() const { return method == HttpMethod::GET; } bool isPOST() const { return method == HttpMethod::POST; } bool isDELETE() const { return method == HttpMethod::DELETE; } }; /** Interface class for generic node of chain of responsibility */ class IHttpHandler { public: virtual void handle(const HttpRequest& req) = 0; virtual void set_next(IHttpHandler* next) = 0; virtual ~IHttpHandler() = default; }; class BasicHttpHandler: public IHttpHandler { protected: /** Note: the keyword 'protected' allows derived classes * (aka subclasses) to access the 'm_next' pointer. But * this keyword forbids any external code from accessing * the pointer m_next. */ IHttpHandler* m_next = nullptr; public: BasicHttpHandler(IHttpHandler* next = nullptr) : m_next(next){ } virtual ~BasicHttpHandler() { delete m_next; m_next = nullptr; } /** Set next request in the chain */ void set_next(IHttpHandler* next) { m_next = next; } }; class HttpGetHandler: public BasicHttpHandler { std::string m_path; public: HttpGetHandler(std::string path): m_path(path){ } void handle(const HttpRequest& req) override { // Checks whether this handler object can process the request. if(req.method == HttpMethod::GET && m_path == req.path) { std::printf( " [RESPONSE] <HttpGetHandler> Return Html and http response with " "status code 200 for resource: %s \n", m_path.c_str()); } else { // If the request cannot be handle, this hanlder forwards the request to // the next handler in the chain. m_next->handle(req); } } }; class HttpUploadHandler: public BasicHttpHandler { void handle(const HttpRequest& req) override { if( req.method == HttpMethod::POST && req.path == "/upload" ) std::printf( " [RESPONSE] <HttpUploadHandler> Uploading data: '%s' \n" , req.payload.c_str()); else m_next->handle(req); } }; class HttpError404Handler: public BasicHttpHandler { void handle(const HttpRequest& req) override { std::printf(" [RESPONSE] <HttpError404Handler>" " Error 404 Not found resource: %s \n", req.path.c_str()); } }; struct HttpServer { // First handler in the chain IHttpHandler* m_hnd; HttpServer() { IHttpHandler* hnd_index = new HttpGetHandler("/"); IHttpHandler* hnd_download = new HttpGetHandler("/download"); hnd_index->set_next(hnd_download); IHttpHandler* hnd_upload = new HttpUploadHandler(); hnd_download->set_next(hnd_upload); IHttpHandler* hnd_error = new HttpError404Handler(); hnd_upload->set_next(hnd_error); m_hnd = hnd_index; } ~HttpServer() { delete m_hnd; m_hnd = nullptr; } void dispatch_request(const HttpRequest& req) { std::printf("\n [TRACE] Process request => path = %s \n", req.path.c_str()); m_hnd->handle(req); std::printf(" ---------------------------------------\n"); } }; int main(int argc, char** argv) { HttpServer server; auto req1 = HttpRequest{ "/", HttpMethod::GET, "" }; server.dispatch_request(req1); auto req2 = HttpRequest{ "/error/resource", HttpMethod::GET, "" }; server.dispatch_request(req2); auto req3 = HttpRequest{ "/upload", HttpMethod::POST, "file data" }; server.dispatch_request(req3); return 0; }
Build and running:
$ g++ chain-of-responsibility2.cpp -o cor2 -std=c++1z -Wall -Wextra $ ./cor2 [TRACE] Process request => path = / [RESPONSE] <HttpGetHandler> Return Html and http response with status code 200 for resource: / --------------------------------------- [TRACE] Process request => path = /error/resource [RESPONSE] <HttpError404Handler> Error 404 Not found resource: /error/resource --------------------------------------- [TRACE] Process request => path = /upload [RESPONSE] <HttpUploadHandler> Uploading data: 'file data' ---------------------------------------
1.13.4 Http Request handling - Implementation 3 (modern c++ style)
This implementation is similar to the implementation 2, but the advantage of the current approach is the usage of smart pointers and less code repetition.
File: chain-of-responsibility3.cpp
#include <iostream> #include <string> #include <memory> enum class HttpMethod { GET, POST, DELETE, PUT, HEAD }; /** Request object - encapsulates a http request */ struct HttpRequest { std::string path = "/"; HttpMethod method = HttpMethod::GET; std::string payload = ""; // Missing - http headers implementation ---- // bool isGET() const { return method == HttpMethod::GET; } bool isPOST() const { return method == HttpMethod::POST; } bool isDELETE() const { return method == HttpMethod::DELETE; } }; /** Interface class for generic node of chain of responsibility */ class IHttpHandler { public: /** Predicate that checks whether request can be handled */ virtual bool can_handle(const HttpRequest& req) = 0; /** Process (aka handle) request */ virtual void do_handle(const HttpRequest& req) = 0; virtual void handle(const HttpRequest& req) = 0; ~IHttpHandler() = default; }; class BasicHttpHandler: public IHttpHandler { std::shared_ptr<IHttpHandler> m_next; public: BasicHttpHandler(std::shared_ptr<IHttpHandler> next = nullptr) : m_next(next){ } virtual ~BasicHttpHandler() = default; void set_next(std::shared_ptr<IHttpHandler> next) { m_next = next; } /** Note: The keyword 'final' causes compile-time error if this * method is overriden by any class derived from this one. */ void handle(const HttpRequest& req) final { if(this->can_handle(req)) this->do_handle(req); else if(m_next != nullptr) m_next->handle(req); else throw std::runtime_error(" There is no handle for this request."); } }; class HttpGetHandler: public BasicHttpHandler { std::string m_path; public: HttpGetHandler(std::string path): m_path(path){ } bool can_handle(const HttpRequest& req) override { return req.method == HttpMethod::GET && m_path == req.path; } void do_handle(const HttpRequest& req) override { std::printf( " [RESPONSE] <HttpGetHandler> Return Html and http response with " "status code 200 for resource: %s \n", m_path.c_str()); } }; class HttpUploadHandler: public BasicHttpHandler { bool can_handle(const HttpRequest& req) override { return req.method == HttpMethod::POST && req.path == "/upload"; } void do_handle(const HttpRequest& req) override { std::printf( " [RESPONSE] <HttpUploadHandler> Uploading data: '%s' \n" , req.payload.c_str()); } }; class HttpError404Handler: public BasicHttpHandler { bool can_handle(const HttpRequest& req) override { return true; } void do_handle(const HttpRequest& req) override { std::printf(" [RESPONSE] <HttpError404Handler>" " Error 404 Not found resource: %s \n", req.path.c_str()); } }; struct HttpServer { std::shared_ptr<IHttpHandler> m_hnd; HttpServer() { auto hnd_index = std::make_shared<HttpGetHandler>("/"); auto hnd_download = std::make_shared<HttpGetHandler>("/download"); hnd_index->set_next(hnd_download); auto hnd_upload = std::make_shared<HttpUploadHandler>(); hnd_download->set_next(hnd_upload); auto hnd_error = std::make_shared<HttpError404Handler>(); hnd_upload->set_next(hnd_error); m_hnd = hnd_index; } void dispatch_request(const HttpRequest& req) { std::printf("\n [TRACE] Process request => path = %s \n", req.path.c_str()); m_hnd->handle(req); std::printf(" ---------------------------------------\n"); } }; int main(int argc, char** argv) { HttpServer server; auto req1 = HttpRequest{ "/", HttpMethod::GET, "" }; server.dispatch_request(req1); auto req2 = HttpRequest{ "/error/resource", HttpMethod::GET, "" }; server.dispatch_request(req2); auto req3 = HttpRequest{ "/upload", HttpMethod::POST, "file data" }; server.dispatch_request(req3); return 0; }
Compiling and running:
$ g++ chain-of-responsibility3.cpp -o cor3 -std=c++1z -Wall -Wextra $ ./cor3 [TRACE] Process request => path = / [RESPONSE] <HttpGetHandler> Return Html and http response with status code 200 for resource: / --------------------------------------- [TRACE] Process request => path = /error/resource [RESPONSE] <HttpError404Handler> Error 404 Not found resource: /error/resource --------------------------------------- [TRACE] Process request => path = /upload [RESPONSE] <HttpUploadHandler> Uploading data: 'file data' ---------------------------------------
1.13.5 Http Request handling - Implementation 4 (modern c++ style + functional)
This implementation of chain-of-responsibility design pattern replaces the linked list of http handlers by an array of handlers. Another modification is the introduction of the class AdapterHandler which allows creating http handlers (http routes) using C++ lambdas which makes possible to create new http handlers without creating new classes. Thus, the class AdapterHandler makes this code more functional-programming friendly.
File: chain-of-responsibility4.cpp.
#include <iostream> #include <string> #include <vector> #include <memory> enum class HttpMethod { GET, POST, DELETE, PUT, HEAD }; /** Request object - encapsulates a http request */ struct HttpRequest { std::string path = "/"; HttpMethod method = HttpMethod::GET; std::string payload = ""; // Missing - http headers implementation ---- // bool isGET() const { return method == HttpMethod::GET; } bool isPOST() const { return method == HttpMethod::POST; } bool isDELETE() const { return method == HttpMethod::DELETE; } }; /** Interface class for generic node of chain of responsibility */ class IHttpHandler { public: /** Predicate that checks whether request can be handled */ virtual bool can_handle(const HttpRequest& req) = 0; /** Process (aka handle) http request and returns http response. */ virtual void handle(const HttpRequest& req) = 0; ~IHttpHandler() = default; }; class HttpGetHandler: public IHttpHandler { std::string m_path; public: HttpGetHandler(std::string path): m_path(path){ } bool can_handle(const HttpRequest& req) override { return req.method == HttpMethod::GET && m_path == req.path; } void handle(const HttpRequest& req) override { std::printf( " [RESPONSE] <HttpGetHandler> Return Html and http response with " "status code 200 for resource: %s \n", m_path.c_str()); } }; /* This class allows creating http handlers (transaction handlers) * using C++ lambdas. */ template<typename predicate_t, typename action_t> class AdapterHandler: public IHttpHandler { predicate_t m_predicate; action_t m_action; public: AdapterHandler(predicate_t predicate, action_t action) : m_predicate(predicate), m_action(action) { } bool can_handle(const HttpRequest& req) override { return m_predicate(req); } void handle(const HttpRequest& req) override { return m_action(req); } }; struct HttpServer { std::vector<std::shared_ptr<IHttpHandler>> m_handlers; HttpServer() { addHandler(std::make_shared<HttpGetHandler>("/")); addHandler(std::make_shared<HttpGetHandler>("/download")); addLambdaHandler( [](const HttpRequest& req) { return req.method == HttpMethod::POST && req.path == "/upload"; } ,[](const HttpRequest& req) { std::printf( " [RESPONSE] <HttpUploadHandler> Uploading data: '%s' \n" , req.payload.c_str()); } ); addLambdaHandler( [](const HttpRequest& req){ return true; } ,[](const HttpRequest& req){ std::printf(" [RESPONSE] <HttpError404Handler>" " Error 404 Not found resource: %s \n", req.path.c_str()); } ); } void addHandler(std::shared_ptr<IHttpHandler> hnd) { m_handlers.push_back(hnd); } template<typename predicate_t, typename action_t> void addLambdaHandler(predicate_t pred, action_t action) { using T = AdapterHandler<predicate_t, action_t>; auto hnd = std::make_shared<T>(pred, action); m_handlers.push_back( hnd ); } void dispatch_request(const HttpRequest& req) { std::printf("\n [TRACE] Process request => path = %s \n", req.path.c_str()); for(const auto& hnd: m_handlers) if(hnd->can_handle(req)) { hnd->handle(req); return; } std::printf(" ---------------------------------------\n"); } }; int main(int argc, char** argv) { HttpServer server; auto req1 = HttpRequest{ "/", HttpMethod::GET, "" }; server.dispatch_request(req1); auto req2 = HttpRequest{ "/error/resource", HttpMethod::GET, "" }; server.dispatch_request(req2); auto req3 = HttpRequest{ "/upload", HttpMethod::POST, "file data" }; server.dispatch_request(req3); return 0; }
Build and running:
$ g++ chain-of-responsibility4.cpp -o cor4 -std=c++1z -Wall -Wextra $ ./cor4 [TRACE] Process request => path = / [RESPONSE] <HttpGetHandler> Return Html and http response with status code 200 for resource: / [TRACE] Process request => path = /error/resource [RESPONSE] <HttpError404Handler> Error 404 Not found resource: /error/resource [TRACE] Process request => path = /upload [RESPONSE] <HttpUploadHandler> Uploading data: 'file data'
1.14 Visitor
1.14.1 Overview
The visitor design pattern is a behavioral design pattern which allows to add new operations to a class hierarchy without modifying classes or adding new methods to derived classes. While the visitor makes easier to add new operators, it makes harder to add new derived classes.
Intent: (GOF):
- "Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates."
Features:
- Define operations to be performed on tree data structures without modifying them or adding new virtual methods.
- Decouple data structure and algorithms.
- Decouple two class hierarchies.
- The code to perform the operation is centralized in the visitor object. If the operation were implemented as methods, the code would be scattered through all derived classes.
Drawbacks:
- The class hierarchy must be stable as adding a new derived class requires modifying all visitors implementations.
Application Domains:
- Process tree-like recursive data structures such as:
- File systems: directories + files
- AST - Abstract Syntax Tree
- XML
- Process a collection of heterogenous derived classes.
- Emulate double dispatching (aka dual dispatching or multi-methods) which is a feature available at Common Lisp (CLOS) and Perl6.
- Process/ evaluate or interpret an AST - Abstract Syntax Tree in many different ways, for instance, evaluate expression, serialize to string, compile, get string representation and so on.
- Serialization
- Game colisions
- Shape intersections
Variations:
- Cyclic Visitor
- Acyclic Visitor
- Decorator
- Extension Object
Alternatives to Visitor Design Pattern:
- If-else statements + RTTI (Runtime Type Identification)
- Boost.Variant or C++17 std::variant
- Multi methods - not possible in C++.
- Possible in Common Lisp, Clojure and so on.
- Pattern Matching + Algebraic Data Types or Classes.
- Possible in functinal laguages such as F# (F Sharp), Scala, Haskell and so on.
Parts:
- Interface: Element
- The element is an element of the class hirarchy which contains the method .accept(Visitor v) which takes a visitor as argument.
- Method: .accept(Visitor v) => Takes a visitor (operation) as argument.
- Interface: Visitor -
- Perform the operations the class hierarchy. Every operation is an implementation of the visitor interface.
- Methods: The visitor interface requires a method visit for
derived class of the interface Element. Note: Element1,
Element2 and Element3 are derived classes of interface Element.
- .visit(Element1 e) => Specific version of the operation for class Element1.
- .visit(Element2 e1) => Specific version of the operation for Element2.
- .visit(Element3 e) => …
- ConcreteElement -> Implementation of interface Element.
- ConcreteVisitor -> Operation for the class hierarchy.
Papers:
- Open Pattern Matching for C++
- Design and evaluation of C++ open multi-methods
https://parasol.tamu.edu/~yuriys/papers/OMM10.pdf
- Note: Shows many use cases for double-dispatch and
- XVF: C++ Introspection by Extensible Visitation
- Design Patterns for Dealing with Dual Inheritance Hierarchies in C++
- Generic Visitors in C++
- Acyclic Visitor
- Using Template Metaprogramming to Enhance Reuse in Visitor-based Model Interpreters
Other implementation examples:
- A polyglot's guide to multiple dispatch - Eli Bendersky's website
- Visitor Pattern ~ Programming Tutorials by SourceTricks
- c++ - Modular Visitor Pattern - Code Review Stack Exchange
- The Visitor Pattern Explained - CodeProject
- foonathan::blog() - Implementation Challenge: Revisiting the visitor pattern
- Why I don't like the visitor pattern - AlBlue’s Blog
- ACCU - Defining Visitors Inline in Modern C++
- Software EngiSneering: C++ Visitors: Basic Implementation
- The Amazing Visitor Pattern | Ruminations
Visitor with C++17 - std::variant
- Chapter 45. Boost.Variant - 1.68.0 (Boost Variant Documentation)
- C++17 hat einen Visitor | heise Developer (In German)
Generic Visitor
- Implementing the visitor pattern using C++ Templates - Stack Overflow
- Cooperative Visitor: A Template Technique for Visitor Creation
- Templated implementation of the Visitor Pattern – Blog – John Wellbelove
Reflection based visitor:
1.14.2 Example: Basic Cyclic Visitor
File:
Online Compiler:
- https://repl.it/repls/LinearVioletredRatio
- IVisitor interace - The visitor interface encapsulates the operation performed on the class hierarchy. It should define a specific version operation (method visit) for each derived class.
// Forward reference class Circle; class Square; class Blob; class IVisitor{ public: virtual ~IVisitor() = default; virtual void visit(Circle& sh) = 0; virtual void visit(Square& sh) = 0; virtual void visit(Blob& sh) = 0; };
Class hierarchy:
- Interface IShape.
// Shape interface - The base class must define the method accept class IShape{ public: virtual ~IShape() = default; virtual void accept(IVisitor& v) = 0; };
- Implementations of the interface class IShape.
class Circle: public IShape{ public: double radius; Circle(double radius): radius(radius) { } void accept(IVisitor& v) override { v.visit(*this); } }; class Square: public IShape{ public: double side; Square(double side): side(side) { } void accept(IVisitor& v) override { v.visit(*this); } }; class Blob: public IShape{ public: Blob(){} void accept(IVisitor& v) override { v.visit(*this); } };
- PrintNameVisitor - Visitor Interface Implementation - This visitor encapsulates an operation which prints the name of each object in the class hierarchy.
// Operation which prints name of the class hierarchy class PrintNameVisitor: public IVisitor{ public: void visit(Circle& sh) override { std::cout << " => Shape is a circle of radius = " << sh.radius << "\n"; } void visit(Square& sh) override { std::cout << " => Shape is a square of side = " << sh.side << "\n"; } void visit(Blob& ) override { std::cout << " => Shape is a blob with an undefined shape" << "\n"; } };
- ComputeAreaVisitor - This stateful visitor encapsulate an operation which computes the area of each element.
class ComputeAreaVisitor: public IVisitor{ private: double _area = 0.0; public: ComputeAreaVisitor() = default; double getArea(){ return _area; } void visit(Circle& sh) override { // CircleArea = PI * radius^2 _area = 3.1415 * sh.radius * sh.radius; } void visit(Square& sh) override { _area = sh.side * sh.side; } void visit(Blob& ) override { _area = 100.0; } };
- FunctionAdapater - Turns lambda function into a visitor object reducing the code boilerplate.
// Lambda function are used for adding new behavior to the object. template<class Result> class FunctionAdapter: public IVisitor{ template<typename T> using Func = std::function<Result (T& sh)>; Result _res; Func<Circle> _fn_circle; Func<Square> _fn_square; Func<Blob> _fn_blob; public: Result get(){ return _res; } FunctionAdapter(Func<Circle> fnCircle, Func<Square> fnSquare, Func<Blob> fnBlob) : _res{} , _fn_circle{fnCircle} , _fn_square{fnSquare} , _fn_blob{fnBlob} { } void visit(Circle& sh) override { _res = _fn_circle(sh); } void visit(Square& sh) override { _res = _fn_square(sh);} void visit(Blob& sh) override { _res = _fn_blob(sh); } };
- Compiling and Running:
$ clang++ visitor1.cpp -o visitor1.bin -g -std=c++14 -Wall -Wextra && ./visitor1.bin
- Experiment 1:
// Sample shapes auto s1 = Circle(3.0); auto s2 = Square(4.0); auto s3 = Blob(); std::cout << "===> Experiment 1: PrintNameVisitor " << "\n"; auto visitor1 = PrintNameVisitor(); s1.accept(visitor1); s2.accept(visitor1); s3.accept(visitor1);
Output:
===> Experiment 1: PrintNameVisitor => Shape is a circle of radius = 3 => Shape is a square of side = 4 => Shape is a blob with an undefined shape
- Experiment 2:
std::cout << "===> Experiment 2: ComputeAreaVisitor " << "\n"; auto visitor2 = ComputeAreaVisitor(); s1.accept(visitor2); std::cout << "Area of shape 1 = " << visitor2.getArea() << "\n"; s2.accept(visitor2); std::cout << "Area of shape 2 = " << visitor2.getArea() << "\n"; s3.accept(visitor2); std::cout << "Area of shape 3 = " << visitor2.getArea() << "\n";
Output:
===> Experiment 2: ComputeAreaVisitor Area of shape 1 = 28.2735 Area of shape 2 = 16 Area of shape 3 = 100
- Experiment 3:
auto visitor3 = FunctionAdapter<std::string>{ [](Circle& ){ return "circle"; }, [](Square& ){ return "square"; }, [](Blob& ){ return "blob"; }, }; s1.accept(visitor3); std::cout << "Type of shape 1 = " << visitor3.get() << "\n"; s2.accept(visitor3); std::cout << "Type of shape 2 = " << visitor3.get() << "\n"; s3.accept(visitor3); std::cout << "Type of shape 3 = " << visitor3.get() << "\n";
Output:
===> Experiment 3: FunctionAdapter Type of shape 1 = circle Type of shape 2 = square Type of shape 3 = blob
- Experiment 4:
std::cout << "===> Experiment 4: FunctionAdapter " << "\n"; // Creates operation to compute shape perimiter auto visitor4 = FunctionAdapter<double>{ [](Circle& s){ return 2 * 3.1415 * s.radius * s.radius ; }, [](Square& s){ return 4.0 * s.side; }, [](Blob& ){ return -100.0; }, }; s1.accept(visitor4); std::cout << "Perimeter of shape 1 = " << visitor4.get() << "\n"; s2.accept(visitor4); std::cout << "Perimeter of shape 2 = " << visitor4.get() << "\n"; s3.accept(visitor4); std::cout << "Perimeter of shape 3 = " << visitor4.get() << "\n";
Output:
===> Experiment 4: FunctionAdapter Perimeter of shape 1 = 56.547 Perimeter of shape 2 = 16 Perimeter of shape 3 = -100
1.14.3 Example: Generic Acyclic Visitor
File:
Online Compiler:
Description: Simple generic acyclic implementation of visitor pattern in C++11. Note: acyclic means that it doesn't have cyclic dependencies.
Template class visitable used the CRTP (Curious Recurring Template Pattern) for making derived classes visitable without virtual methods.
Shape interface class:
// Shape interface - The base class must define the method accept class IShape{ public: virtual ~IShape() = default; };
Visitable CRTP Template class
template<typename Implementation> class VisitableShape: public IShape{ public: /** Accept any visitor class which implements * the method: * void Visitor::visit(Implementation& impl); *********************************************/ template<typename Visitor> void accept(Visitor&& v) { v.visit(static_cast<Implementation&>(*this)); } };
Visitable class hierarchy:
class Circle: public VisitableShape<Circle> { public: double radius; Circle(double radius): radius(radius) { } }; class Square: public VisitableShape<Square> { public: double side; Square(double side): side(side) { } }; class Blob: public VisitableShape<Blob> { public: Blob(){} };
The following template class turns a list of lambda function parameters into a visitor object.
template<class Result> class FunctionAdapter{ // Lambda function are used for adding new behavior to the object. template<typename T> using Func = std::function<Result (T& sh)>; Result _res; Func<Circle> _fn_circle; Func<Square> _fn_square; Func<Blob> _fn_blob; public: Result get(){ return _res; } FunctionAdapter(Func<Circle> fnCircle, Func<Square> fnSquare, Func<Blob> fnBlob) : _res{} , _fn_circle{fnCircle} , _fn_square{fnSquare} , _fn_blob{fnBlob} { } void visit(Circle& sh) { _res = _fn_circle(sh); } void visit(Square& sh) { _res = _fn_square(sh);} void visit(Blob& sh) { _res = _fn_blob(sh); } template<class Visitable> Result operator()(Visitable& visitable){ this->visit(visitable); return this->get(); } };
Some visitors implementation:
auto visitorGetName = FunctionAdapter<std::string>{ [](Circle& ){ return "circle"; }, [](Square& ){ return "square"; }, [](Blob& ){ return "blob"; }, }; // Creates operation to compute shape perimeter auto visitorGetArea = FunctionAdapter<double>{ [](Circle& s){ return 2 * 3.1415 * s.radius * s.radius ; }, [](Square& s){ return 4.0 * s.side; }, [](Blob& ){ return -100.0; }, };
Running in REPL:
>> .L visitor2.cpp >> auto s1 = Circle(3.0); >> auto s2 = Square(4.0); >> auto s3 = Blob(); >> s1.accept(visitorGetName) >> visitorGetName.get() "circle" >> s2.accept(visitorGetName) >> visitorGetName.get() "square" >> visitorGetName(s1) "circle" >> visitorGetName(s2) "square" >> visitorGetArea(s1) (double) 56.547000 >> s1.accept(visitorGetArea) >> visitorGetArea.get() (double) 56.547000 >> visitorGetArea(s2) (double) 16.000000 >> s2.accept(visitorGetArea) >> visitorGetArea.get() (double) 16.000000 >> visitorGetArea(s2) (double) 16.000000 >> visitorGetArea(s3) (double) -100.00000 >>
1.14.4 Example: Visitor implemented with C++17 std::variant
The type std::variant and the function std::visit from C++17 standard can simplify the implementation of visitor design pattern decreasing the amount of boilerplate code and the coupling between classes. In fact, by using std::visit, no class modification is required to implement this pattern.
- Note: If the current compiler doesn't support C++17 ISO Standard, a workaround is to use Boost.Variant library, which is the predecessor of C++17's std::variant.
File:
Online Compiler:
Headers used:
#include <iostream> #include <cstdio> #include <string> #include <memory> #include <functional> #include<type_traits> // C++17 Variant std::variant and std::visit #include <variant>
Classes:
class Circle{ public: double radius; Circle(double radius): radius(radius) { } }; class Square{ public: double side; Square(double side): side(side) { } }; class Blob{ public: Blob(){} };
Simple visitor:
- Implements operation which prints the description of an element in the class "hierarchy".
struct PrintNameVisitor{ void operator()(const Circle& sh){ std::cout << " SHAPE = Circle of radius " << sh.radius << std::endl; } void operator()(const Square& sh){ std::cout << " SHAPE = Square of radius " << sh.side << std::endl; } void operator()(const Blob&){ std::cout << " SHAPE = Blob - no one knows how it looks like. " << std::endl; } };
Generic Visitor:
- Turns 3 lambda functions ,which processes each element, into a visitor object.
/** Visitor which adpts a function */ template<typename Result> class FunctionVisitor{ private: template<typename Input> using FN = std::function<Result (const Input&)>; FN<Circle> fn_circle; FN<Square> fn_square; FN<Blob> fn_blob; public: FunctionVisitor(FN<Circle> fnCircle, FN<Square> fnSquare, FN<Blob> fnBlob) : fn_circle(fnCircle) ,fn_square(fnSquare) ,fn_blob(fnBlob){ } //Result get() const { return result; } Result operator()(const Circle& sh){ return fn_circle(sh); } Result operator()(const Square& sh){ return fn_square(sh); } Result operator()(const Blob& sh){ return fn_blob(sh); } };
- Higher order function makeVisitor, turn 3 lambda functions into a single visitor lambda function.
template<typename Input, typename Result> using FnVisit = std::function<Result (const Input&)>; /** Make a visitor using a functional-programming way * Visitor with lambdas look like "pattern matching" * from functional languages. */ template<typename Result = void> auto makeVisitor(FnVisit<Circle, Result> fnCircle, FnVisit<Square, Result> fnSquare, FnVisit<Blob, Result> fnBlob ){ return [=](auto&& a){ using C = std::decay_t<decltype(a)>; if constexpr(std::is_same_v<C, Circle>){ return fnCircle(a); } if constexpr(std::is_same_v<C, Square>){ return fnSquare(a); } if constexpr(std::is_same_v<C, Blob>){ return fnBlob(a); } std::cerr << "WARNING - Not returned valid result"; return Result{}; }; } //-- EoF makeVisitor ----//
Variant type Shape:
- An instance of this type can be a Circle, Square or Blob.
using Shape = std::variant<Circle, Square, Blob>;
Sample shapes:
// Sample shapes Shape s1 = Circle(3.0); Shape s2 = Square(4.0); Shape s3 = Blob();
- Experiment Code 1
std::puts("\n === EXPERIMENT 1 ================="); std::visit(printVisitor, s1); std::visit(printVisitor, s2); std::visit(printVisitor, s3);
Output:
=== EXPERIMENT 1 ================= SHAPE = Circle of radius 3 SHAPE = Square of radius 4 SHAPE = Blob - no one knows how it looks like.
- Experiment Code 2
std::puts("\n === EXPERIMENT 2 ================="); auto visitorGetName = FunctionVisitor<std::string>{ [](const Circle& ) { return "circle"; }, [](const Square& ) { return "square"; }, [](const Blob& ) { return "blob"; } }; std::cout << "Type of shape 1 = " << std::visit(visitorGetName, s1) << "\n"; std::cout << "Type of shape 2 = " << std::visit(visitorGetName, s2) << "\n"; std::cout << "Type of shape 3 = " << std::visit(visitorGetName, s3) << "\n";
Output:
=== EXPERIMENT 2 ================= Type of shape 1 = circle Type of shape 2 = square Type of shape 3 = blob
- Experiment Code 3
std::puts("\n === EXPERIMENT 3 ================="); // Creates operation to compute shape perimeter auto visitorGetArea = FunctionVisitor<double>{ [](const Circle& s){ return 3.1415 * s.radius * s.radius ; }, [](const Square& s){ return s.side * s.side; }, [](const Blob& ){ return 0.0 / 0.0; /* NAN Not a number */ }, }; std::cout << "Area of shape 1 = " << std::visit(visitorGetArea, s1) << "\n"; std::cout << "Area of shape 2 = " << std::visit(visitorGetArea, s2) << "\n"; std::cout << "Area of shape 3 = " << std::visit(visitorGetArea, s3) << "\n";
Output:
=== EXPERIMENT 3 ================= Area of shape 1 = 28.2735 Area of shape 2 = 16 Area of shape 3 = nan
- Experiment Code 4
std::puts("\n === EXPERIMENT 4 ================="); auto fnVisitorArea = makeVisitor<double>( [](const Circle& s){ return 3.1415 * s.radius * s.radius ; } ,[](const Square& s){ return s.side * s.side; } ,[](const Blob& ){ return 0.0 / 0.0; /* NAN Not a number */ } ); std::cout << "Area of shape 1 = " << std::visit(fnVisitorArea, s1) << "\n"; std::cout << "Area of shape 2 = " << std::visit(fnVisitorArea, s2) << "\n"; std::cout << "Area of shape 3 = " << std::visit(fnVisitorArea, s3) << "\n";
Output:
=== EXPERIMENT 4 ================= Area of shape 1 = 28.2735 Area of shape 2 = 16 Area of shape 3 = nan
2 C++ Specific Design Pattern and Idioms
2.1 Erase-Remove Idiom
The algorithms std::remove and std::remove_if don't remove anythign from a container, they just move the elements supposed to be removed to the end of the container. The erase/remove idiom is a just a simple technique for really removing the elements from the container.
Example:
- The following session in CERN's ROOT REPL shows that std::remove does not remove elements of deque zs equal to 1. In order to really remove them, it is necessary to use COTAINER.erase method.
// =======>>> BEFORE <<<============// >> auto zs = std::deque<int> {1, 10, 1, 2, 2, 5, 10, 1, 1, 1, 5, 2}; >> zs { 1, 10, 1, 2, 2, 5, 10, 1, 1, 1, 5, 2 } // =======>>> std::remove <<<============// // Remove all elements equal to 1 >> auto it = std::remove(zs.begin(), zs.end(), 1); // Elements are not removed >> zs { 10, 2, 2, 5, 10, 5, 2, 1, 1, 1, 5, 2 } // Iterator >> it (std::_Deque_iterator<int, int &, int *> &) @0x7f1eb9a99060 // Now elements are removed. >> zs.erase(it, zs.end()) (std::deque<int, std::allocator<int> >::iterator) @0x3b50df0 // ==========>>> AFETER <<<=================== >> zs (std::deque<int, std::allocator<int> > &) { 10, 2, 2, 5, 10, 5, 2 }
For short
- Note: It not only works for deque, it works for all STL iterators.
>> auto elems = std::deque<int> {1, 10, 1, 2, 2, 1, 1, 5, 10, 1, 1, 1, 5, 2}; // Before removing >> elems { 1, 10, 1, 2, 2, 1, 1, 5, 10, 1, 1, 1, 5, 2 } >> // Really remove all elements equal to 1. >> elems.erase(std::remove(elems.begin(), elems.end(), 1), elems.end()) (std::deque<int, std::allocator<int> >::iterator) @0x3bc3e80 // After removing >> elems { 10, 2, 2, 5, 10, 5, 2 } >>
The boilerplate code can be reduced by defining generic functions (templated functions) which operates on any container, namely erase_equal and erase_if.
Function: erase_equal
Remove all elements equal to some value.
// Erase all elements of a container equal to a given value template< class T ,template<class> class Allocator ,template<class, class> class Container > void erase_equal(Container<T, Allocator<T>>& container, const T& value){ auto it = std::remove(container.begin(), container.end(), value); container.erase(it, container.end()); }
Usage example for deque:
>> std::deque<int> elems1 = {1, 10, 1, 2, 2, 1, 1, 5, 10, 1, 1, 1, 5, 2}; >> elems1 { 1, 10, 1, 2, 2, 1, 1, 5, 10, 1, 1, 1, 5, 2 } >> erase_equal(elems1, 1) >> elems1 (std::deque<int> &) { 10, 2, 2, 5, 10, 5, 2 } >> erase_equal(elems1, 10) >> elems1 { 2, 2, 5, 5, 2 } >>
Usage example for vector:
//========= VECTOR ====================== auto words = std::vector<std::string>{ "c++", "breakthrough", "c++", "RUST", "asm", "ARM", "RISC", "CISC", "asm", "forth", "forth", "c++", "low level", "RUST", "asm" }; >> words { "c++", "breakthrough", "c++", "RUST", "asm", "ARM", "RISC", "CISC", "asm", "forth", "forth", "c++", "low level", "RUST", "asm" } >> // Error: Cannot infer type of of parameter T //------------------------------------------- >> erase_equal(words, "c++") ROOT_prompt_6:1:1: error: no matching function for call to 'erase_equal' erase_equal(words, "c++") ^~~~~~~~~~~ input_line_15:6:8: note: candidate template ignored: deduced conflicting types for parameter 'T' ('std::__cxx11::basic_string<char>' vs. 'char [4]') void erase_equal(Container<T, Allocator<T>>& container, const T& value){ // Now it works >> erase_equal(words, std::string("c++")) // Other possibility >> erase_equal<std::string>(words, "c++") >> words { "breakthrough", "RUST", "asm", "ARM", "RISC", "CISC", "asm", "forth", "forth", "low level", "RUST", "asm" } >> >> erase_equal<std::string>(words, "asm") >> words { "breakthrough", "RUST", "ARM", "RISC", "CISC", "forth", "forth", "low level", "RUST" } >>
Function: erase_if
Remove all elements matching a predicate function:
// Erase all elements of a container equal to a given value template< class T ,class Predicate /* Some predicate function */ ,template<class> class Allocator ,template<class, class> class Container > void erase_if(Container<T, Allocator<T>>& container, Predicate predicate){ auto it = std::remove_if(container.begin(), container.end(), predicate); container.erase(it, container.end()); }
>> std::deque<int> adeque = {6, 10, 6, 7, 5, 1, 4, 2, 1, 6, 10, 2, 9, 8, 1, 1, 5, 2}; >> adeque { 6, 10, 6, 7, 5, 1, 4, 2, 1, 6, 10, 2, 9, 8, 1, 1, 5, 2 } >> // Remove all elemets less than 5 >> erase_if(adeque, [](int x){ return x < 5; }) >> adeque (std::deque<int> &) { 6, 10, 6, 7, 5, 6, 10, 9, 8, 5 } >>
References:
- Erase-remove Idiom Revisited - CodeProject
- More C++ Idioms/Erase-Remove - Wikibooks, open books for an open world
- Difference between std::remove and vector::erase for vectors - GeeksforGeeks
Further Reading:
2.2 Virtual Friend Function Idiom
A C++ friend function is a free function with access to a class protected and private member variables and member functions. Unlike virtual member functions (aka virtual methods), which are resolved at runtime and can be overridden by each derived class, friend functions cannot be declared as virtual and be overridden by each derived class for customizing the function behavior for each different implementation. The virtual friend function idiom emulates the virtual member function by forwarding the method call to a member function.
This technique can be used for implementing stream insertion (<<) and extraction operator for reading and writing derived classes from streams.
In the following code, the idiom is used for printing a class hierarchy and customizing the behavior of the stream insertion operator (<<), which is a friend function, for each derived class.
File: virtual-friend-function.cpp
#include <iostream> #include <string> // Shape interface class IShape { // Protected =>> Means that only derived classes // can access this function. protected: // Print object to the stream object virtual void print_me(std::ostream& os) const = 0; public: virtual void draw() const = 0; virtual ~IShape() = default; // Friend function => (Just a free function) friend std::ostream& operator<<(std::ostream& os, const IShape& shape) { shape.print_me(os); return os; } }; class Circle: public IShape { private: double radius = 0.0; void print_me(std::ostream& os) const override { os << " IShape(Cicle) with radius = " << radius; } public: Circle(double radius): radius(radius){ } void draw() const override { // Implementation here .... } }; class Square: public IShape { private: double length = 0.0; void print_me(std::ostream& os) const override { os << " IShape(Square) with length = " << length; } public: Square(double length): length(length){ } void draw() const override { // Implementation here .... } }; int main(int argc, char** argv) { Square shapeA{5}; Circle shapeB{10}; /* * virtual-friend-function.cpp: In function ‘int main(int, char**)’: * virtual-friend-function.cpp:67:30: error: * ‘virtual void Square::print_me(std::ostream&) const’ is private * within this context | shapeA.print_me(std::cout); */ // shapeA.print_me(std::cout); // Compile-time error. std::cout << " [INFO] shape A = " << shapeA << '\n'; std::cout << " [INFO] shape B = " << shapeB << '\n'; return 0; }
Building:
$ g++ virtual-friend-function.cpp -o out.bin -std=c++1z -Wall -Wextra -g
Running:
$ ./out.bin [INFO] shape A = IShape(Square) with length = 5 [INFO] shape B = IShape(Cicle) with radius = 10
2.3 RAII idiom - Resource Aquisition Is Initialization
2.3.1 Overview
Comes from book: C++ Programming with Design Patterns Revealed
RAII is a design pattern which takes advantage of C++'s deterministic destructor feature for deallocating resources such as pointers to objects allocated on the heap memory, database handlers, socket handlers and etc. The RAAI technique uses an object allocated on the stack which acquires the resource as a constructor argument and performs the resource cleanup in the destructor method which is called when when the wrapper object goes out scope or an exception happens.
- Note: This pattern is specific for C++, for Java and Scala use try finally statements. Python has the with statement and C# has the using statement. In addition, this pattern needs the deterministic destructor feature which is unique to C++.
Alternative or more descriptive names:
- CADRE => Constructor Acquires, Destructor Releases
- SBRM => Scope-Bound Resource Management.
Note: It is no longer necessary to design any class for RAII since C++11 has smart pointers which already implements this pattern. However, it is important to understand how this idiom works and the problem that it solves.
- In C++ >= C++ 11, it is better to use
unique_ptr
for handling resources as shown at Objects Own Resources (RAII)
RAII is the process of:
- Acquiring a resouce and wrapping it as an object's state.
- Using the resource
- Releasing the resource in the object's destructor member function which is executed when the object goes out of escope.
Problems solved by this technique.
- Avoid memory leak
- Avoid resource leak
- Save and restore context. => An object allocated on the stack could be used to set the current directory and restore saved directory when the destructor is invoked when the object goes out scope. The same technique could be used to save and restore std::cout or std::cerr flags.
Known uses:
- C++11 shared_ptr (Smart pointer)
- C++11 unique_ptr (Smart pointer)
- Almost all STL containers/collections which manages heap-memory behind the scene, namely, std::vector, std::list, std::map, … and so on.
Example:
Naive code - If an exception happens, the resource will not be released and the code will be vulnerable to memory leak or resource leak.
Handler* handle = getDatabaseHandler(); // Throw exception => Resource leak performInvalidOperation(handle); ... // Forget to release resource! deleteResource(handle);
Or:
Object* heapObject = new Object(param0, param1, ....); peformOperation(heapObject); heapObject->method1; // Throw exception!! ==> Memory Leak! // If the user forget this statement, // a memory leak will happen. delete heapObject;
Solution - RAAI idiom
- An object is used to control the lifetime of a resource ensuring that is released when the object goes out of scope or an exception happens. The resource is aquired by the constructor and realeased by the destructor.
template<class Resource> class RaaiHandler { public: // Consructor: Acquire resource on the constructor RaaiHandler(Resource* rawHandle_) : (rawHandle_) {}; // Destructor: Releases resource // The destructor is always called when the object goes out of scope // or an exception happens. ~RaiiHandler() { delete _handler; } private: RaiiHandler* _handler; }; // Object created without new keyword is allocated in the stack, not in the heap. RaiiHandler hnd(createNewResource()); // .. . Peform operation Operation(hnd); // Once the escope is gone, the resource is released.
2.3.2 Example - Heap-allocated memory
Another more specific RAAI Example:
- In this example, the resource acquired in the constructor, a heap-allocated array of type T, is released at the destructor when a HeapArray object goes out of scope. All STL containers such as std::vector, std::map, std::deque and so on uses RAII technique for avoiding memory leak by deleting the heap-allocated objects when no longer needed.
#include <iostream> #include <string> #include <ostream> template<typename T> class HeapArray{ private: size_t m_size; T* m_resource; public: // Resource is acquired at constructor HeapArray(size_t size, T init) : m_size(size), m_resource(new (std::nothrow) T[size]) { std::cout << " [LOG] Intialize array with size = " << size << " and init = " << init << std::endl; if(m_resource != nullptr) for(size_t i = 0; i < size; i++) m_resource[i] = init; } // Delegated constructor on the right size HeapArray(size_t size): HeapArray(size, T{}){ } // Resource is released at destructor ~HeapArray() { std::cout << " [LOG] Destructor -> Delete array" << std::endl; // Always safe to delete delete [] m_resource; m_resource = nullptr; } // Forbid copy constructor HeapArray(const HeapArray&) = delete; // Forbid copy-assignment operator HeapArray& operator= (const HeapArray&) = delete; // Move constructor HeapArray(HeapArray&& rhs) { std::cout << " [LOG] Move constructor " << std::endl; std::swap(this->m_resource, rhs.m_resource); } // Move assignment operator HeapArray& operator= (HeapArray&& rhs) { std::cout << " [LOG] Move assignment operator " << std::endl; std::swap(this->m_resource, rhs.m_resource); return *this; } size_t size() const { return m_size; } T& operator[](size_t n){ return m_resource[n]; } friend auto operator<< (std::ostream& os, const HeapArray<T>& rhs) -> std::ostream& { os << "[" << rhs.m_size << "]( "; for(size_t i = 0; i < rhs.m_size; i++) os << rhs.m_resource[i] << " "; os << ")"; return os; } };
Usage example:
>> .L raai1.cpp >> auto vb = HeapArray<double>(4, 3.5); [LOG] Intialize array with size = 4 and init = 3.5 >> std::cout << "vb = " << vb << std::endl; vb = [4]( 3.5 3.5 3.5 3.5 ) >> vb[0] = 10.0; >> vb[1] = 20.0; >> vb[2] = -10.2; >> std::cout << "vb = " << vb << std::endl; vb = [4]( 10 20 -10.2 3.5 )
References
2.3.3 Example - Managing Resource with custom wrapper
Resource is everything that needs to be released or cleaned up after no longer needed. Some examples of resource are file descriptors, handlers, socket handlers, data base handlers and heap allocated memory. The RAAI technique can help managing resources since it ensures that a resource cleanup always will happen after the wrapper object goes out of scope.
Example:
In the C-file API, the resource is the FILE* file pointer. The API requies the client code to call fclose after the pointer is no longer needed. Despite that this case is applied to the C-file API, it can applied to any other type of resource.
Note: It is better to use the C++ file API since it is more C++ friendly and already has RAII. The C-file API was used in this section for simulating a generic resource.
API:
#include <cstdio> // <stdio.h> in C. // Open file getting the resource FILE* FILE* fopen(const char *pathname, const char *mode); // Release resource. int fclose(FILE* stream); int fputs(const char* s, FILE* stream); int putc(int c, FILE* stream);
Solution 1: Use a custom RAAI wrapper object.
#include <iostream> #include <cstdio> template<class Resource> class RAIIWrapper{ public: using Cleanup_fn = std::function<void (Resource*)>; // Constructor: Acquire resource // Parameter res => Wrapped Resource // Parameter cleanup => Clenup function which releases resources. RAIIWrapper(Resource* res, Cleanup_fn cleanup) :m_res(res), m_cleanup(cleanup) { } // Forbid copy-constructor RAIIWrapper(const RAIIWrapper& rhs) = delete; // Forbid copy-assignment operator RAIIWrapper& operator= (const RAIIWrapper& rhs) = delete; // Move constructor => Transfer resource ownership // from teporary object rhs (R-value) to new object (this). RAIIWrapper(RAIIWrapper&& rhs){ this->m_res = rhs.m_res; rhs.m_res = nullptr; } // Move assignment operator - move resource ownership // from temporary object (rhs) in the right hand-size of (=) // to this object. RAIIWrapper& operator= (RAIIWrapper&& rhs){ std::swap(this->m_res, rhs.m_res); return *this; } void close(){ if(m_res != nullptr) m_cleanup(m_res); m_res = nullptr; std::cerr << "LOG cleanup resource."; } // Destructor => Release resource ~RAIIWrapper(){ this->close(); } Resource* get() const { return m_res; } // Conversion operator to check whether wrapped pointer is null. operator bool() const { return m_res != nullptr; } private: Resource* m_res; Cleanup_fn m_cleanup; };
Sample client code:
void writeFile(const char* file){ // The wrapper automatically release the wrapped resource when out of scope RAIIWrapper<FILE> fptr(::fopen(file, "w"), &fclose); if(!fptr){ ::fputs(" [ERROR] Failed to create file\n", stderr); return; } ::fputs("Testing line 1\n", fptr.get()); ::fputs("Testing line 2\n", fptr.get()); ::fputs("C++17 is AWESOME! AMAZING!\n", fptr.get()); }
Test:
>> writeFile("/tmp/raii-resource.txt") LOG cleanup resource.>> >> writeFile("/etc/raii-resource.txt") [ERROR] Failed to create file LOG cleanup resource.>> >> // Check created file. >> .! cat /tmp/raii-resource.txt Testing line 1 Testing line 2 C++17 is AWESOME! AMAZING! >>
2.3.4 Example - Managing Resource with smart pointer
The smart pointer std::unique_ptr is not only suitable for managing heap-allocated objects, it is also useful for managing resources.
#include <iostream> #include <memory> // Smart pointers #include <functional> // std::function // Used for simulating a generic resource. // FILE*, fopen, fclose .... #include <cstdio> // Type alias for smart pointer with custom deleter template<typename T> using Resource = std::unique_ptr<T, std::function<void (T*)>>; void writeFile(const char* file) { // The wrapper automatically release the resource when out of scope Resource<FILE> fptr(::fopen(file, "w"), // Custom-deleter function [](FILE* hnd){ std::cerr << " [TRACE] File closed OK." << std::endl; if(hnd != nullptr) ::fclose(hnd); }); if(!fptr){ ::fputs(" [ERROR] Failed to create file\n", stderr); return; } ::fputs("Testing line 1\n", fptr.get()); ::fputs("Testing line 2\n", fptr.get()); ::fputs("C++17 is AWESOME! AMAZING!\n", fptr.get()); } >> writeFile("/tmp/resource1.txt"); [TRACE] File closed OK. >> .! cat /tmp/resource1.txt Testing line 1 Testing line 2 C++17 is AWESOME! AMAZING! >> writeFile("/boot/resource1.txt"); [ERROR] Failed to create file >>
2.4 PIMPL idiom - Pointer to Implementation
The PIMPL - Pointer to Implementation idiom, also known as compiler firewall, is a widely used technique in C++ for completly hiding class private members in the public header file. This technique uses an opaque pointer to an internal non declared class in the current header file encapsulating all fields of the outer class.
Also known as:
- PIMPL - Pointer to Implementation
- Compiler firewall idiom
- Cheshire Cat
Motivation and Benefits:
- Reduce compilation time as the number of #include headers in the class header is not changed.
- Makes the class data member in the public header really private and obfuscated.
- Changing of private members of the class which are encapsulated in the opaque pointer class does not require recompilation of client code. This feature is very important for library development as it avoid client code recompilation as the public headers are not changed.
- Less likely to breaking binary compatibility - ABI Application Binary Interface.
- Note: Changing class member variables or member functions (methods) breaks the ABI and requires the recompilation of client code. The pimpl maintains the binary compatibility by not changing class' private members.
- TL;DR
- PIMPL: Allow changes to implementation without the need to recompile client code.
Drawbacks:
- More complexity and work for API implementators.
- Not convenient when there are protected members which needs to be accessed by subclasses.
- Runtime performance overhead due to the pointer indirection.
Example: All private members of class CashFlow are conained in the opaque type Impl which is not defined in the header file.
- file: CashFlow.h -> Class public interface.
#ifndef _CashFlow_H_ #define _CashFlow_H_ // C++ 11's smart pointers #include <memory> class CashFlow{ private: // Forward declaration of incomplete type (Implementation). struct Impl; // This opaque type encapsulate the outer class' private member // Pointer to implementation (PIMPL) std::unique_ptr<Impl> m_pimpl; // Explicit Default ctor ~CashFlow = default; public: // Default ctor CashFlow(); void show(); void add(double x); int size(); // Net Present Value - NPV of cash flow for a given rate of return double npv(double rate); // Compute IRR - Internal rate of return double irr(); }; #endif // --- EOF ---- //
- file: CashFlow.cpp -> Class implementation.
// C++ 11's smart pointers #include <memory> #include "CashFlow.h" // Struct is just class with all members // public by default struct CashFlow::Impl{ // cash flow vector std::vector<double> m_clf; // Default ctor Impl(){} // Default dtor ~Impl() = default; }; // Ctor CashFlow::CashFlow(): m_pimpl(new Impl()) {} void CashFlow::add(double x){ m_pimpl->m_clf.push_back(x); } double CashFlow::get(int i){ return m_pimpl->m_clf[i]; } ... ... ... ....
References and further:
- Pimpl Idiom - <>
- Best Friends: C++11 Move Semantics and Pimpl - https://www.embeddeduse.com/2016/05/30/best-friends-cpp11-move-semantics-and-pimpl/
- Improving C++ Encapsulation with the Pimpl Idiom - https://visualstudiomagazine.com/articles/2012/11/29/the-pimpl-idiom-in-c-plus-plus.aspx
- Why every C++ developer should know about the pimpl idiom pattern - https://tonka2013.wordpress.com/2013/08/31/why-every-c-developer-should-know-about-the-pimpl-idiom-pattern/
- Passing Containing Parent to pimpl idiom implementation class - http://www.sharprobotica.com/2010/04/passing-containing-parent-to-pimpl-idiom-implementation-class/
- Dive in to C++ and survive - https://www.embedded.com/print/4008235
- Modern and Lucid C++ Advanced for Professional Programmers - https://wiki.ifs.hsr.ch/CppAdvanced/files/lecture_12_advanced_library_design.pdf
2.5 Non-copiable class idiom
Classes where copying doesn't make sense such as classes managing resources or singletons should have the copy constructor and copy assignment operator no accessible to any external code. It can be done by making the both member functions private or by annotating them as deleted (C++11).
- Note: resource means anything that needs to be disposed when no longer needed such as heap memory, database handlers, socket handlers, file descriptors and etc.
Prior to C++11
Notes:
- The copy-constructor and copy-assignment operators should be declared private, even if they are not declared the copiler will generate both by default what can lead to unexpected bugs.
- If the following class used the default copy constructor and copy-assignment operator, the socket handler would be copied, but the initial class and the copy would still use the same resource, in this case connection. Then, if one of the classes went out of scope, the destructor would be invoked disposing the resource. Therefore, it would close the connection and make the socket unavailable to the remaining class and any attempt to use the disposed resource would result in a runtime error hard to debug.
//=========>> file: socket.hpp - Header <<===========; class SocketWrapper{ public: // Constructor SocketWrapper(); // Destructor ~SocketWrapper(); // Move consttructor [OPTIONAL] SocketWrapper(SocketWrapper&& rhs); // Move-assignment operator [OPTIONAL] SocketWrapper& operator=(SocketWrapper&& rhs); private: // Copy constructor SocketWrapper(const SocketWrapper&); // Copy assignment operator SocketWrapper& operator=(const SocketWrapper&); // Resource - socket file descriptor int m_sockfd; }; //====>>> file: socket.cpp - Implementation <<<========== SocketWrapper::SocketWrapper(){ m_sockfd = ::socket(AF_INET, SOCK_STREAM, 0); } SocketWrapper::~SocketWrapper(){ ::close(m_sockfd); } // Forbidden copy constructor SocketWrapper::SocketWrapper(const SocketWrapper&) { } // Forbidden copy-assignemnt operator SocketWrapper& operator=(const SocketWrapper&) { } // Move constructor SocketWrapper::SocketWrapper(SocketWrapper&& rhs){ std::swap(this.m_sockfd, rhs.m_sockfd); } // Move assignment-operator SocketWrapper& SocketWrapper::operator=(SocketWrapper&& rhs){ std::swap(this.m_sockfd, rhs.m_sockfd); return *this; }
C++ >= C++11
- In C++11, it is just necessary to annotate both copy constructor and assignment operator with delete to make them non accessible. Any attempt to copy a class with those member functions annotated as deleted will result in a compile-time error.
//=========>> file: socket.hpp - Header <<===========; class SocketWrapper{ public: // Constructor SocketWrapper(); // Destructor ~SocketWrapper(); // Copy constructor SocketWrapper(const SocketWrapper&) = delete; // Copy assignment operator SocketWrapper& operator=(const SocketWrapper&) = delete; // Move consttructor [OPTIONAL] SocketWrapper(SocketWrapper&& rhs); // Move-assignment operator [OPTIONAL] SocketWrapper& operator=(SocketWrapper&& rhs); private: // Resource - socket file descriptor int m_sockfd; }; //====>>> file: socket.cpp - Implementation <<<========== SocketWrapper::SocketWrapper(){ m_sockfd = ::socket(AF_INET, SOCK_STREAM, 0); } SocketWrapper::~SocketWrapper(){ ::close(m_sockfd); } // Move constructor SocketWrapper::SocketWrapper(SocketWrapper&& rhs){ std::swap(this.m_sockfd, rhs.m_sockfd); } // Move assignment-operator SocketWrapper& SocketWrapper::operator=(SocketWrapper&& rhs){ std::swap(this.m_sockfd, rhs.m_sockfd); return *this; }
Non-copiable parent class trick
The class could also be made non-copiable using the following trick used by boost::noncopyable noncopyable:
- NonCopyable base class: Any class inheriting this class (private inheritance) will be non-copiable.
class NonCopyable { protected: NonCopyable() = default; ~NonCopyable() = default; // Disable default copy-constructor NonCopyable(const NonCopyable&) = delete; // Disable default copy assignment operator const NonCopyable& operator=(const NonCopyable&) = delete; };
- Derived class. (Note: private inheritance). Any attempt to perform copy operations with copy constructor or copy assignment operator, results in compile-time error.
//=========>> file: socket.hpp - Header <<===========; class SocketWrapper: private NonCopyable { public: // Constructor SocketWrapper(); // Destructor ~SocketWrapper(); // Move consttructor [OPTIONAL] SocketWrapper(SocketWrapper&& rhs); // Move-assignment operator [OPTIONAL] SocketWrapper& operator=(SocketWrapper&& rhs); private: // Resource - socket file descriptor int m_sockfd; };
2.6 Copy and swap idiom
The copy-and-swap idiom is a widely used technique for implementing the rule-of-three by defining the copy-assignment operator with a custom swap function. In C++11, the idiom can also be used for implementing the rule-of-five by defining the move constructor and move assignment operator with a swap function.
Notes:
- The copy-assignment operator is needed by the rule-of-three (pre-C++11) which states that a class managing a resouce, which implements copy constructor, copy assignment operator and a destructor, should all of them.
- The move-assignment operator is needed by the rule-of-five (C++11) which states that a class, which wraps some resource implementing a copy constructor, copy assignment operator, move constructor, move assignment operator and a destructor, should implement all of them.
- Note: This idiom is only applicable to copiable classes which manages some resource.
Benefits:
- Less code duplication.
- Just a swap to rule them all for implementing the copy-assignment operator, move assignment operator and move constructor in C++11.
The following member functions are necessary to implement this idiom:
- Copy-constructor
- Destructor
- Swap function which can swap the data of two objects of same
class. This function all member variables of both objects.
- DO NOT use std::swap as this function.
- This functions must be non-throwing.
Example
The following redundant class String manages a resource, a heap-allocated array of characters.
- File: file:src/design-patterns/copy-swap1.cpp
- Gist: copy-swap1.cpp
- Online Compiler: https://rextester.com/XWJA10133
class String { public: // Default constructor String(); String(size_t size, char ch); String(const char* text); // Copy-constructor (rule-of-three and rule-of-five) String(const String& rhs); // Move-constructor (rule-of-three and rule-of-five) String(String&& rhs); // Destructor (rule-of-three and rule-of-five) ~String(); // Copy-assignment operator (rule-of-three and rule-of-five) String& operator= (const String& rhs); // Move-assignment operator (rule-of-five) String& operator= (String&& rhs); String operator+(const char* text); // Swap function (std::swap overload) // needed by the copy-and-swap idiom. friend void swap(String& lhs, String& rhs); // Make class printable - note this operator is a member // function of class ostream, no from this class. friend std::ostream& operator<<(std::ostream& os, const String& rhs); size_t size(); const char* data(); private: // String size size_t m_size; // Resource: Pointer to first string character char* m_data; };
The idiom needs the following functions:
- Copy constructor
String::String(const String& rhs){ this->m_size = rhs.m_size; this->m_data = new (std::nothrow) char [rhs.m_size + 1]; std::copy(rhs.m_data, rhs.m_data + rhs.m_size + 1, this->m_data); }
- Destructor
String::~String() { // Check for null to avoid dangling pointer. if(m_data != nullptr) delete [] m_data; m_data = nullptr; m_size = 0; }
- Swap function which can be a member function or a friend function overload of std::swap. In this case, it was used the friend std::swap version.
// std::swap overload - Implementation of // => friend void swap(String& lhs, String& rhs) void swap(String& lhs, String& rhs){ std::swap(lhs.m_data, rhs.m_data); std::swap(lhs.m_size, rhs.m_size); }
The copy-assignment operator, move-assignment operator and move constructor are implemented with the custom std::swap overload.
- Copy-assignment operator:
- The temporary copy is used for disposing the current's object data which is transferred to the temporary object and deleted when the object goes out of scope.
// Copy-assignment operator (rule-of-three and rule-of-five) // Copy-and-swap implementation String& String::operator= (const String& rhs) { // Temporary copy String temp(rhs); swap(*this, temp); return *this; }
- Move constructor (C++11, Rule of five)
// Move-constructor String::String(String&& rhs) : String() // Delegated constructor { swap(*this, rhs); }
- Move assignment operator (C++11, Rule of five)
// Move-assignment operator (rule-of-five) // Transfer resource ownership from right hand-side object // to this object being instantiated. String& String::operator= (String&& rhs){ swap(*this, rhs); return *this; }
Useful C++ Documentation
- Copy assignment operator - cppreference.com
- std::swap - cppreference.com (Header <utility>)
- swap - C++ Reference
Reference and further reading
- Want Speed? Pass by Value. « C++Next
- Copy-swap transaction
- More C++ Idioms/Copy-and-swap - Wikibooks, open books for an open world
- C++: Vor- und Nachteile des d-Zeiger-Idioms, Teil 1 | heise Developer
- Note: In German.
- More C++ Idioms/Non-throwing swap - Wikibooks, open books for an open world
- Copy-and-swap - C++ Patterns
- Copy-and-Swap Idiom in C++ - GeeksforGeeks
- C/C++ - The perfect Copy-And-Swap idiom usage · GitHub
- c++ - Applying the copy-swap idiom to humans and employees - Code Review Stack Exchange
- C++: More on Implementing Move Assignment
- c++ - Move Assignment incompatible with Standard Copy and Swap - Stack Overflow
2.7 Interface Class idiom
2.7.1 Overview
Unlike C# and Java, C++ doesn't have any keyword for implementing interface, however it can be implemented by creating a class with only pure virtual functions, in other words, only abstract methods or methods without implementation.
The interface class has a runtime overhead due to the virtual methods that are resolved at runtime. An alternative solution when the virtual methods calls performance overhead is not acceptable, is to use generic programming or templates which doesn't have runtime cost since the methods to be called are resolved at compile-time.
An interface class should:
- be on the header file, for instance, IStack.hpp or any other header file. It does not need any implementation .cpp file.
- not contain any member variable
- any concrete methods (non pure virtual member functions)
- contain only virtual member function, aka abstract methods or methods without implementation.
- Always define a default virtual destructor member function function. Otherwise, memory leaks may happen as the destructors of derived classes will not be called.
Applications:
- Use multiple implementations
- An interface class allows a client code, depending on the interface and no concrete implementation, to be able use multiple implementations and swap or replace them at runtime.
- Parallell development and team working
- If the interface is kept the same and is stable, the client code and multiple implementations can be developed in parallel by different people and teams.
- Avoid the diamond-of-death problem
- Most programming languages avoid the infamous diamond-of-death problem caused by multiple inheritance by allowing a class to inherit from multiple interfaces and only a single parent concrete class or abstract class.
- See
- Mock objec for testing
- If the client code depends only on the interface, an object used by the client code, for instance, a database object (class RealDatabase) implemeting the interface IDatabase which connects to a real database can be replaced by a mock or dummy object (class DummyDatabase) that simulates a database and is not connect to any real database.
- Plugin systems
- For instance an application could load the classes implementing a common interfaces class called IIinterface, class ClassA from the shared library (file: pluginA.dll on Windows or pluginA.so on an Unix-like OS) and the class ClassB from the plugin file pluginB.so. Those plugins or shared libraries containing the classes implementing the common interface can be loaded and unloaded at runtime using the operating system APIs, LoadLibrary on Windows and dlopen on UNix-like operating systems (Linux, MacOSX, BSD…). The benefits of a plugin system are: extensibility of functionality at runtime, reload classes at runtime, user extensions, faster compile-time and easier update.
- Potential ABI problem: If the application and its plugins are built with different C++ compilers, the application may not be able to load the classes provided by the plugins due to the C++ lack of standard ABI - Application Binary Interface. The application its plugins only will be binary compatible if the interface class uses oly C-compatible types in its member functions (methods) signatures. It means that the interface class cannot use C++ types in its member functions such as STL containers, std::string, references, tuples and so on. A binary compatible interface can only int, double, pointer to void void*, pointer to clases, function pointers and so on.
Alternative:
- Interfaces have a runtime overhead due to the runtime resolution of virtual member functions. In cases where this performance penalty is not acceptable, an alternative to the interface approach (dynamic polymorphism) is to use template metaprogramming and templated functions since they can work with any class satisfying the type requirements regardless if they inherit from the same base class. Another alternative is to use CRTP - Curious Recurring Template Pattern which can emulate inheritance (dynamic polymorphism) with static polymorphism. The drawback of the template metaprogramming approaches is the lack of runtime polymorphism, since classes not inheriting from a common base class cannot be stored in the STL container or be referred with the same pointer.
Interface Class Declaration:
class IStack{ public: virtual ~IStack() = default; virtual int size() const = 0; virtual void push(double x) = 0; virtual double pop() = 0; virtual double peek() const = 0; };
Note:
- The annotation virtual - means that the method (member function) can be overriden by the derived class. Methods in the base class not annotated as virtual cannot be overriden in the derived classes. Unlike Java, C++ methods are not virtual by default.
- The annotation (=0) - means a pure virtual member function, aka pure virtual function which is an abstract method, method without implementation.
Or:
struct IStack{ virtual ~IStack() = default; virtual int size() const = 0; virtual void push(double x) = 0; virtual double pop() = 0; virtual double peek() const = 0; };
Or using C++ auto keyword for functions:
struct IStack{ virtual ~IStack() = default; virtual auto size() const -> int = 0; virtual auto push(double x) -> void = 0; virtual auto pop() -> double = 0; virtual auto peek() const -> double = 0; };
2.7.2 Known Uses
In Windows COM (Component Object Model) architechture, a client code can load classes at runtime without knowing anything about their implementations. All COM classes and COM intefaces are required to implement the interface IUnknown which allows a client code to query at runtime the interfaces implemented by the class and get pointers to those interfaces.
The COM Architechture has a plugin-system design, In-process COM servers which provides COM one or more COM-compatible classes are DLL shared libraries providing a set of standard functions. A client-code instantiates COM classes using the Windows API function CoGetClassObject that locates the shared libraries using the unique class ID (CLSDI) and IID (Interface Identifier) in the Windows registry. The CLSID is used for locating the shared library file (*.dll) containing COM classes and the IID is used to set the interface pointer returned by the function CoGetClassObject. The C++ ABIs issues are circunvented because all COM interfaces uses only C-compatible types in their member functions.
Windows API COM Interfaces:
Interface IUnknown
- Base interface required to be implemented by all interfaces and concrete COM classes.
- IID (Interface ID): {00000000-0000-0000-C000-000000000046}
struct IUnknown { virtual HRESULT __stdcall QueryInterface (REFIID riid, void **ppvObject) = 0; virtual ULONG AddRef () = 0; virtual ULONG Release () = 0; };
Interface IDispatch
- Allows querying which methods a class implements and calling class methods at runtime without knowing them at compile-time.
- IID (Interface ID): {00020400-0000-0000-C000-000000000046}
class IDispatch : public IUnknown { public: /** Checks whether if the object can provide a type library. */ virtual HRESULT GetTypeInfoCount(/* [OUT] */ unsigned int * pctinfo) = 0; /** Get the type library */ virtual HRESULT GetTypeInfo( /* [IN] */ unsigned int iTInfo, /* [OUT] */ LCID lcid, /* [IN] */ ITypeInfo** ppTInfo ) = 0; /** Find the numeric ID of some object's method or property */ virtual HRESULT GetIDsOfNames( /* [IN] */ REFIID riid, /* [IN] */ OLECHAR** rgszNames, /* [IN] */ unsigned int cNames, /* [IN] */ LCID lcid, /* [OUT] */ DISPID* rgDispId ) = 0; /** Call a property or method. */ virtual HRESULT Invoke( DISPID dispIdMember, /* [IN] */ REFIID riid, /* [IN] */ LCID lcid, /* [IN] */ WORD wFlags, /* [IN] */ DISPPARAMS * pDispParams, /* [IN] */ VARIANT * pVarResult, /* [IN] */ EXCEPINFO * pExcepInfo, /* [OUT] */ unsigned int * puArgErr ) = 0; };
2.7.3 Example
Complete Code Example
Interface class declaration:
// Interface Stack. (Should be placed in the header file.) class IStack{ public: virtual ~IStack() = default; virtual int size() const = 0; virtual void push(double x) = 0; virtual double pop() = 0; virtual double peek() const = 0; };
Example: Interface implementations.
- Implementation of interface IStack using vector as internal representation.
class StackVector: public IStack{ public: StackVector(){} StackVector(const std::initializer_list<double>& xs){ _stack.insert(_stack.begin(), xs.begin(), xs.end()); } int size() const { return _stack.size(); } void push(double x){ _stack.push_back(x); } double pop(){ if(this->size() == 0) throw std::runtime_error("Error: stack is empty"); double top = _stack.back(); _stack.pop_back(); return top; } double peek() const { if(this->size() == 0) throw std::runtime_error("Error: stack is empty"); return _stack.back(); } private: std::vector<double> _stack{}; };
- Implementation using deque as internal representation:
class StackDeque: public IStack{ public: StackDeque(){} StackDeque(const std::initializer_list<double>& xs){ _stack.insert(_stack.begin(), xs.begin(), xs.end()); } int size() const { return _stack.size(); } void push(double x){ _stack.push_back(x); } double pop(){ if(this->size() == 0) throw std::runtime_error("Error: stack is empty"); double top = _stack.back(); _stack.pop_back(); return top; } double peek() const { if(this->size() == 0) throw std::runtime_error("Error: stack is empty"); return _stack.back(); } private: std::deque<double> _stack{}; };
- Sample client code:
auto stack_sum(IStack& s) -> double{ //std::cerr << " ==> stack_sum for references" << std::endl; double sum = 0.0; if(s.size() == 0) return sum; while(s.size() != 0) sum += s.pop(); return sum; } auto stack_sum(IStack* s) -> double{ // std::cerr << " ==> stack_sum for pointers" << std::endl; double sum = 0.0; if(s->size() == 0) return sum; while(s->size() != 0) sum += s->pop(); return sum; }
Function main:
StackVector sv = {1.0, 2.0, 3.0, 5.0, 6.0}; StackDeque sd = {1.0, 2.0, 3.0, 5.0, 6.0}; // The same client code works with any implementation of the interface. std::cout << "stack_sum(sv) = " << stack_sum(sv) << std::endl; std::cout << "stack_sum(sd) = " << stack_sum(sd) << std::endl; IStack* spointer = nullptr; StackVector sv2 = {1.0, 2.0, 3.0, 5.0, 6.0}; StackDeque sd2 = {1.0, 2.0, 3.0, 5.0, 6.0}; spointer = &sv2; std::cout << "stack_sum(spointer) = " << stack_sum(spointer) << std::endl; spointer = &sd2; std::cout << "stack_sum(spointer) = " << stack_sum(spointer) << std::endl; auto sptr = std::unique_ptr<IStack, std::function<void (IStack*)>>{ nullptr, // Custom deleter [](IStack* p){ std::cerr << " ==== Stack deleted OK" << std::endl ; delete p; } }; sptr.reset(new StackVector()); sptr->push(10); sptr->push(25.0); sptr->push(20.0); std::cout << "stack_sum(sptr) = " << stack_sum(*sptr) << std::endl; sptr.reset(new StackDeque()); sptr->push(10); sptr->push(25.0); sptr->push(20.0); std::cout << "stack_sum(sptr) = " << stack_sum(*sptr) << std::endl;
Compiling Running:
$ clang++ interface-class.cpp -o interface-class.bin -g -std=c++1z -Wall -Wextra $ ./interface-class.bin stack_sum(sv) = 17 stack_sum(sd) = 17 stack_sum(spointer) = 17 stack_sum(spointer) = 17 stack_sum(sptr) = 55 ==== Stack deleted OK stack_sum(sptr) = 55 ==== Stack deleted OK
References and further reading:
2.8 CRTP - Curious Recurring Template Pattern
2.8.1 Overview
It is a variation of GOF template design pattern where an algorithm defined by the base class is customized or specified by the derived class. However, unlike the GOF one, this version uses C++ template metaprogramming for emulating dynamic polymorphism or inheritance at compile time. So it makes the code faster by eliminating virtual function-calls.
Features:
- Coined by James Coplien - 1995
- Static polymorphism technique based on template metaprogramming for speeding up the code eliminating virtual functions.
Use cases:
- Reduce virtual function call overhead - by simulating dynamic polymorphism through static polymorphism.
- Implement state machines.
- Implement high performance numerical libraries.
- Code injection.
Libraries using this pattern:
- Boost.Iterator
- Boost.Python
- Boost.Serialization
Also known as:
- Code injection
- Barton‐Nackman Trick
- Mixin - Name used outside C++ community
2.8.2 Eliminating Virtual Member Function
Approach 1 - GOF - OOP template pattern
- Code 1: GOF Template method design pattern using virtual functions.
class IntervalSummation{ public: // Algorithm or entry point which calls the derived class method. // This is the template method double summation(int lower, int upper) const{ double sum = 0; for(int i = lower; i <= upper; i++) sum += this->stepFn(i); return sum; } protected: // Hook method or to be defined by the derived class virtual double stepFn(double x) const = 0 ; }; class SumOfSquares: public IntervalSummation{ private: double stepFn(double x) const { return x * x; } }; class SumOfCubes: public IntervalSummation{ private: double stepFn(double x) const { return x * x * x; } }; void clientCode(const IntervalSummation& obj){ std::cout << "Summation at [0, 15] = " << obj.summation(0, 15) << std::endl; }
Approach 2 - CRTP generic programming pattern
- Code 2: Code rewritten using CRTP for eliminating virtual function calls. The advantage is that this code can run faster than the previous one, however the cost is the higher complexity, loss of readability and runtime polymorphism. For instance, now is not possible to store multiple implementations of IntervalSummation in a data structure or refer to them with the same pointer.
- File: file:src/design-patterns/crtp1.cpp
- Online Compiler: https://rextester.com/ZLZH2040
template<class Implementation> class IntervalSummation{ public: // Get reference to implementation Implementation& self(){ return *static_cast<Implementation*>(this); } // Overload method const Implementation& self() const { return *static_cast<Implementation const * const>(this); } double summation(int lower, int upper) const { double sum = 0; for(int i = lower; i <= upper; i++) sum += self().stepFn(i); return sum; } }; class SumOfSquares: public IntervalSummation<SumOfSquares>{ public: double stepFn(double x) const { return x * x; } }; class SumOfCubes: public IntervalSummation<SumOfCubes>{ public: double stepFn(double x) const { return x * x * x; } }; template<class T> void clientCode(const IntervalSummation<T>& obj){ std::cout << "Summation at [0, 15] = " << obj.summation(0, 15) << std::endl; }
Running:
$ clang++ crtp.cpp -o crtp.bin -std=c++1z -Wall -Wextra && ./crtp.bin Sum of squares in [0, 10] = 385 Sum of cubes in [0, 10] = 3025 Summation at [0, 15] = 1240 Summation at [0, 15] = 14400
References:
- C++: Prefer Curiously Recurring Template Pattern (CRTP) to Template Pattern - CodeProject
- CRTP-based platform-dependent optimizations | GNSS C++ solutions
- http://stevedewhurst.com/once_weakly/once-weakly20170328/once-weakly20170328.pdf
- http://enki-tech.blogspot.com/2012/08/c11-generic-singleton.html][Enki">Technical Blog: C++11: A generic Singleton]]
- IDENTIFYING PROGRAMMING IDIOMS IN C++ GENERIC LIBRARIES - https://etd.ohiolink.edu/rws_etd/document/get/kent1259116053/inline
- Using CRTP to easily hijack operators in c++11 | masaers’ blog
- https://github.com/nojhan/crtp_functor_ttp
- Developer::Pipelines | Building More Flexible Types with Mixins
- https://accu.org/index.php/journals/296][ACCU">Better Encapsulation for the Curiously Recurring Template Pattern]]
- Replacing Virtual Methods with Templates
- http://gsd.web.elte.hu/lectures/bolyai/2018/mixin_crtp/mixin_crtp.pdf
- https://faithandbrave.hateblo.jp/entry/20071206/1196934096
- Virtual Methods vs CRTP Benchmark – Native Coding
- https://nativecoding.wordpress.com/2015/01/11/important-c-idioms/
- http://barngoggles.com/visitor-with-crtp/
Best Links:
- https://nativecoding.wordpress.com/2015/01/11/important-c-idioms/
- https://marcoarena.wordpress.com/2012/04/29/use-crtp-for-polymorphic-chaining/
- http://thothonegan.tumblr.com/post/157363120503/crtp-curiously-recurring-template-pattern
Videos:
- Curiously Recurring Template Pattern (CRTP) - https://www.youtube.com/watch?v=C3Pi5GlIfjs
2.8.3 Generating operator overloading free functions
The CRTP design pattern can be used for generating operator free functions and avoiding code repetition when implementing them.
- File: file:src/design-patterns/crtp-operators.cpp
- Online Compiler: https://rextester.com/PBI36673
Example:
Class EqualityOperator:
- Requirement: the Impl class must implement the member function:
- bool T::equal(T const& rhs) const
template<typename Impl> class EqualityOperator { public: friend bool operator==(Impl const& lhs, Impl const& rhs) { return lhs.equal(rhs); } friend bool operator!=(Impl const& lhs, Impl const& rhs) { return lhs.equal(rhs); } };
Class AddOperator:
- Imlements the operator (+) free functions.
- Requirement => The class Impl must implement the member function:
- Impl Impl::add(T const& T) const
template<typename Impl, typename T> struct AddOperator { public: friend Impl operator+(Impl const& lhs, T const& rhs) { return lhs.add(rhs); } friend Impl operator+(T const& lhs, Impl const& rhs) { return rhs.add(lhs); } };
Class StreamInsertionOperator:
- Implements the operator (<<) stream-insertion operator for the class Impl.
- Requirement: The class Impl must implement the following member function:
- void printme(std::ostream& os) const
template<typename Impl> struct StreamInsertionOperator { public: friend std::ostream& operator<<(std::ostream& os, Impl const& rhs) { rhs.printme(os); return os; } };
Class Point2D:
- Inherits and all previous CRTP classes and implements their requirements (implicit interface).
class Point2D: public EqualityOperator<Point2D> , public AddOperator<Point2D, int> , public StreamInsertionOperator<Point2D> { public: int x; int y; Point2D(int x, int y): x(x), y(y) { } // Required by: void printme(std::ostream& os) const { os << "Point2D[ x = " << x << " ; y = " << y << " ] " << std::endl; } private: // Allows class EqualityOperator<Point2D> to access private // members of this class. friend class EqualityOperator<Point2D>; friend struct AddOperator<Point2D, int>; // Required by: EqualityOperator<Point2D> bool equal(Point2D const& rhs) const { return x == rhs.x && y == rhs.y; } // Required by: AddOperator<Point2D, int> Point2D add(int rhs) const { return Point2D(this->x + rhs, this->y + rhs); } };
Function: main()
Point2D p1(10, 20); Point2D p2(25, 30); Point2D p3(10, 20); std::cout << std::boolalpha; std::cout << " p1 === p2 ? => " << (p1 == p2) << std::endl; std::cout << " p1 === p3 ? => " << (p1 == p3) << std::endl; std::cout <<" p1 + 10 = " << p1 + 10 << std::endl; std::cout <<" 10 + p1 = " << 10 + p1 << std::endl;
Output of main():
$ ./crtp-operators.bin p1 === p2 ? => false p1 === p3 ? => true p1 + 10 = Point2D[ x = 20 ; y = 30 ] 10 + p1 = Point2D[ x = 20 ; y = 30 ]
2.8.4 CRTP with more concise notation
The following code uses the convenience templated function rcast<> provide a more concise notation for expressing the code intent and takes care of const qualifiers.
File: crtp.cpp
#include <iostream> #include <string> #include <iomanip> // Convenience function for CRTP pattern. template<typename Dest, typename Src> inline Dest& rcast(Src* td){ return *reinterpret_cast<Dest*>(td); } // Convenience templated function for CRTP pattern. This overload can be // called from member functions functions with const annotation. Such as: // ReturType MyClass::someMemberFunc(arg0, ...) const template<typename Dest, typename Src> inline const Dest& rcast(const Src* td){ return *reinterpret_cast<const Dest*>(td); } template<typename Impl> struct CurvePlotter { // Version 1 => Using convenience function rcast<T>(), which // saves typing and expresses the intent in a more explicit way. // void tabulate_1(double start, double stop, double step) const { std::cout << " ======== Version 1 =========" << '\n'; rcast<Impl>(this).show_function_name(); for(double x = start; x < stop; x += step) { double y = rcast<Impl>(this).eval(x); std::cout << std::setw(10) << x << std::setw(10) << y << '\n'; } } // Version 2 => Implementation 2 => Without using convenience function. void tabulate_2(double start, double stop, double step) const { std::cout << " ======== Version 2 =========" << '\n'; reinterpret_cast<Impl const*>(this)->show_function_name(); for(double x = start; x < stop; x += step) { double y = reinterpret_cast<const Impl*>(this)->eval(x); std::cout << std::setw(10) << x << std::setw(10) << y << '\n'; } } }; class LinearPlotter: public CurvePlotter<LinearPlotter> { double m_a, m_b; public: LinearPlotter(double a, double b): m_a(a), m_b(b) { } void show_function_name() const { std::cout << " Curver name = linear " << '\n'; } double eval(double x) const { return m_a * x + m_b; } }; class QuadraticPlotter: public CurvePlotter<QuadraticPlotter> { double m_a, m_b, m_c; public: QuadraticPlotter(double a, double b, double c): m_a(a), m_b(b), m_c(c){ } void show_function_name() const { std::cout << " Curver name = quadratic " << '\n'; } double eval(double x) const { return m_a * x * x + m_b * x + m_c; } }; int main() { auto plt_lin = LinearPlotter(3.0, 5.0); plt_lin.tabulate_1(-5.0, 6.0, 1.0); plt_lin.tabulate_2(-5.0, 6.0, 1.0); auto plt_quad = QuadraticPlotter(2.0, -10.0, 25.0); plt_quad.tabulate_1(-5.0, 6.0, 1.0); plt_quad.tabulate_2(-5.0, 6.0, 1.0); }
Building and running:
$ clang++ crtp.cpp -o out.bin -std=c++1z -g -Wall -Wextra
$ ./out.bin
======== Version 1 =========
Curver name = linear
-5 -10
-4 -7
-3 -4
-2 -1
-1 2
0 5
1 8
2 11
3 14
4 17
5 20
======== Version 2 =========
Curver name = linear
-5 -10
-4 -7
-3 -4
-2 -1
-1 2
0 5
1 8
2 11
3 14
4 17
5 20
======== Version 1 =========
Curver name = quadratic
-5 125
-4 97
-3 73
-2 53
-1 37
0 25
1 17
2 13
3 13
4 17
5 25
======== Version 2 =========
Curver name = quadratic
-5 125
-4 97
-3 73
-2 53
-1 37
0 25
1 17
2 13
3 13
4 17
5 25
2.9 Type Erasure Pattern
2.9.1 Overview
Type erasure is a set of techniques for providing an uniform interface for many different types by hiding the type information from the client code. In C++ type erasure can be implemented with a combination of object oriented programming and generic programming, in other words, inheritance and templates. The fundamental building blocks of this pattern are a base class, called concept, which provides the uniform interface to the wrapped types and a derived templatized class, called model, inherting the concept class which adapts the wrapped type to the concept class. The inheritance allows any template instantation of the model to be treated as it was the base class, thus this approach hides the type information which can be later recovered by downcasting the base class to the derived class.
- Definition by Dave Abrahams and Aleksey Curtovoy in, C++ Template Metaprogramming.
In its fullest expression, type erasure is the process of turning a wide variety of types with a common interface into one type with that same interface
Parts:
- Concept class
- base class - definines the interface being enforced.
- Model class
- templatized class inherting the concept class adpating the wrapped type to the concept class and holding an instance of the wrapped type.
- Type Erasure class (outter class)
- Both the concept and model classes are private inner classes of the type erasure class.
- The type erasure class takes an instance of the model class in the constructor.
- This class stores a pointer variable to the concept class, but storing a pointer to the model class (dynamic polymorphism).
- Wrapped Type or Objects
- Types wrapped by the model class wich will be erased.
Use-cases:
- Create a common interface for many different types.
- Store wrapped types without a common base class in STL containers.
- Store templatized classes in STL containers.
- Implement dynamic or runtime polymorphism with value semantics.
- Take advantage of the commonality of many unrelated classes without a common base class without modifying their source code.
Known uses in C++ standard library:
- std::function (C++11, formber Boost.Function)
- std::any (C++17, former Boost.Any)
- A container which can store anything and the type of the stored object is not known at runtime.
- std::variant (C++17, former Boost.Variant)
- Provides an interface for sum types or disjoint union or visitor OO design pattern which is useful for manipulating abstract syntax tree, creating interpreters, tree data structures and fixed class hierarchies.
- void* - Void pointer in many C-APIs.
References and further reading:
- Nevin Liber, Type Erasure http://files.meetup.com/1455470/Type Erasure.pdf
- c++ - Type erasure: Retrieving value - type check at compile time - Stack Overflow
- Episode Nine: Erasing the Concrete
- c++ - Concept based polymorphism - Code Review Stack Exchange
- Dynamic Programming with Virtual Concepts
- Type erased concepts : cpp
- C++ type erasure - C++ Articles
- Type erasure with unified call syntax
- My Internet Weblog - Type Erasure in C++
- ACCU - Polymorphism in C++ – A Type Compatibility View
2.9.2 Example 1 - Simple type erasure.
Problem: Handle the classes A, B and C which don't have a common base class using dynamic (aka runtime) polymorphism taking advantage of their commonality, the method .getName(). Note: the source code of A, B and C aren't allowed to be modified
- Complete source code:
- File: file:src/design-patterns/type-erasure1.cpp
- Online Compiler: https://rextester.com/XFH16030
class A{ public: std::string getName() const { return "class A"; } void sayA(){ std::cout << "I am the class A" << "\n"; } }; class B{ public: std::string getName() const { return "class B"; } void sayB(){ std::cout << "I am the class B" << "\n"; } }; class C{ public: std::string getName() const { return "class C"; } void sayC() const { std::cout << "I am the class C" << "\n"; } };
Solution: Type erasure design pattern.
class TypeErasure{ private: // --- Forward declarations ---- class Concept; template<class T> class Model; // --- Member Variables ----- // std::shared_ptr<Concept> _concept_ptr; // Optional: // RTTI (runtime type information) for recovering wrapped type // by downcasting const std::type_info& _tinfo; public: template<typename T> TypeErasure(const T& obj) : _concept_ptr(std::make_shared<Model<T>>(obj)) ,_tinfo(typeid(T)) { } auto getName() const -> std::string { return _concept_ptr->getName(); } // Recover reference to wrapped type template<typename T> auto recover() -> T { if(typeid(T) != _tinfo) throw std::runtime_error("Error: cannot cast to this type"); // Note: static_cast downcasting to wrong type has undefined behavior, // use with care! return static_cast<Model<T>*>(_concept_ptr.get())->_obj; } template<typename T> auto hasType() -> bool { return _tinfo == typeid(T); } private: // Concept class defines the interface to be enforced // In general, it is an interface class, a class with only pure virtual // methods (abstract methods), in other words methods without implementation. class Concept{ public: virtual auto getName() const -> std::string = 0; virtual ~Concept() = default; }; // Adapt the wrapped type (T) to the concept template<typename T> class Model: public Concept { public: // Instance of the wrapped type T _obj; // Initialize _opj by copying the parameter Model(const T& obj): _obj(obj){} auto getName() const -> std::string { return _obj.getName(); } }; };
Testing in Cling REPL:
>> .L type-erasure1.cpp >> >> A() (A) @0x1fc2970 >> A().getName() (std::string) "class A" >> B().getName() (std::string) "class B" >> >> B().sayB() I am the class B >> auto tlist = std::deque<TypeErasure>(); tlist.emplace_back(A()) tlist.emplace_back(B()) tlist.emplace_back(C()) >> for(const auto& t: tlist) { std::cout << "Class type = " << t.getName() << "\n"; } Class type = class A Class type = class B Class type = class C >> >> tlist.at(0).recover<A>() (A) @0x20cc590 >> tlist.at(0).recover<A>().sayA() I am the class A >> tlist.at(0).recover<A>().getName() (std::string) "class A" >> >> tlist.at(1).recover<B>().getName() (std::string) "class B" >> >> tlist.at(1).recover<C>() Error in <TRint::HandleTermInput()>: std::runtime_error caught: Error: cannot cast to this type >>
Main function:
auto tlist = std::deque<TypeErasure>(); tlist.emplace_back(A()); tlist.emplace_back(B()); tlist.emplace_back(C()); std::cout << "\n" << "EXPERIMENT 1 ============" << "\n"; for(const auto& t: tlist) { std::cout << "Class type = " << t.getName() << "\n"; } // Note: It is a copy! A objA = tlist.at(0).recover<A>(); objA.sayA(); std::cout << "\n" << "EXPERIMENT 2 ============" << "\n"; // Simulate downcasting failure try { B objB = tlist.at(0).recover<B>(); objB.sayB(); } catch(const std::runtime_error& ex){ std::cout << " [FAILURE]" << ex.what() << "\n"; } B objB = tlist.at(1).recover<B>(); objB.sayB(); auto objC = tlist.at(2).recover<C>(); objC.sayC();
Compiling and running: (File: file:src/design-patterns/type-erasure1.cpp)
$ clang++ type-erasure1.cpp -o type-erasure1.bin -g -std=c++1z -Wall -Wextra $ ./type-erasure1.bin EXPERIMENT 1 ============ Class type = class A Class type = class B Class type = class C I am the class A EXPERIMENT 2 ============ [FAILURE]Error: cannot cast to this type I am the class B I am the class C
2.9.3 Example 2 - Template type erasure - property system
Problem:
In the following code the templated class TProperty<T> encapsulates Get/Set properties. The problem is to put multiple properties with different types such as TProperty<int>, TProperty<double> and TProperty<std::string> in the same container, std::vector, std::map and so on.
/** Class that encapsulate get/set properties * @tparam - Type default constructible, copiable and equality-comparable */ template <typename T> class TProperty { std::string m_name; T m_value; std::type_info const& m_tinfo; public: TProperty(std::string name, T const& init = T{}) : m_name(std::move(name)) , m_value(init) , m_tinfo(typeid(T)) { } std::string Name() const { return m_name; } const std::type_info& Type() { return m_tinfo; } TProperty<T> Get() const { return m_value; } TProperty<T>& Set(T const& value) { std::cerr << " [TRACE] Property [" << m_name << "] set to value = " << value << std::endl; m_value = value; } friend std::ostream& operator<<(std::ostream& os, TProperty<T> const& rhs) { return os << " Property{ Name = " << std::quoted(rhs.m_name) << " ; Value = " << rhs.m_value << " }"; } };
Sample usage: (function main)
TProperty<int> p1("count", 100); TProperty<double> p2("range", 5.6); TProperty<std::string> p3("name", "Box"); std::cout << "Before setting" << std::endl; std::cout << "p1 = " << p1 << "\n"; std::cout << "p2 = " << p2 << "\n"; std::cout << "p3 = " << p3 << "\n"; std::cout << "\n After setting" << std::endl; p1.Set(20); p2.Set(80.50); p3.Set("Square"); std::cout << "p1 = " << p1 << "\n"; std::cout << "p2 = " << p2 << "\n"; std::cout << "p3 = " << p3 << "\n";
Solution:
The solution for the requirement of referring to multiple properties using the same pointer or "storing" properties with different types in the same container is to use type erasure through inheritance, or make the template class inherit from some base class or base interface class.
- File: file:src/design-patterns/property-type-erasure2.cpp
- Online Compiler: https://rextester.com/JTC56116
Interface class IProperty:
template <typename T> class TProperty; class IProperty { public: virtual std::string Name() const = 0; virtual std::type_info const& Type() const = 0 ; virtual void Print(std::ostream& os) const = 0; virtual ~IProperty() = default; // Note: Adding Non virtual methods does not causes break the base class ABI // or binary compatibility with derived classes (fragile-base class problem). template<typename T> T Get() const { if(this->Type() != typeid(T)) throw std::bad_cast(); return static_cast<TProperty<T> const*>(this)->Get(); } template<typename T> void Set(T const& value) { if(this->Type() != typeid(T)) throw std::bad_cast(); static_cast<TProperty<T>*>(this)->Set(value); } template<typename T> TProperty<T>& As() { if(this->Type() != typeid(T)) throw std::bad_cast(); return *static_cast<TProperty<T>*>(this); } template<typename T, typename Visitor> void Visit(Visitor& visitor) { if(this->Type() != typeid(T)) throw std::bad_cast(); static_cast<TProperty<T>*>(this)->Visit(visitor); } // Make class printable friend std::ostream& operator<<(std::ostream& os, IProperty const& rhs) { rhs.Print(os); return os; } };
Class template TProperty:
- As the class TProperty inherits the base class IProperty, all template instances TProperty<int>, TProperty<double> and so on can be referred by the same pointer and "stored" in the same container.
/** Class that encapsulate get/set properties * @tparam - Type default constructible, copiable and equality-comparable */ template <typename T> class TProperty: public IProperty { std::string m_name; T m_value; std::type_info const* m_tinfo; public: TProperty(std::string name, T const& init = T{}) : m_name(std::move(name)) , m_value(init) , m_tinfo(&typeid(T)) { } ~TProperty() = default; std::string Name() const { return m_name; } const std::type_info& Type() const { return *m_tinfo; } T Get() const { return m_value; } TProperty<T>& Set(T const& value) { std::cerr << " [TRACE] Property [" << m_name << "] set to value = " << value << std::endl; m_value = value; return *this; } void Print(std::ostream& os) const { os << " Property{ Name = " << std::quoted(m_name) << " ; Value = " << m_value << " }"; } void PrintValue(std::ostream& os) const { os << m_value; } template<typename Visitor> void Visit(Visitor& visitor) { visitor.visit(m_value); } };
Function makeProperty:
- Creates some property with type specified by calling code (client code).
- Any property created TProperty<T> will be seen by the calling code as the same object IProperty.
template<typename T> std::shared_ptr<IProperty> make_property(std::string const& name, T const& init = T{}) { return std::make_shared<TProperty<T>>(name, init); }
Usage (main function):
std::vector<std::shared_ptr<IProperty>> plist; plist.push_back(make_property<int>("basis-points", 100)); plist.push_back(make_property<double>("price", 5.6)); plist.push_back(make_property<std::string>("product", "")); std::cout <<"\n ======= Experiment 1 =======" << std::endl; int i = 0; for(const auto& p: plist) std::cout << " => p[" << i++ << "] = " << *p << "\n"; std::cout <<"\n ======= Experiment 2 =======" << std::endl; TProperty<int>& p0 = plist[0]->As<int>(); p0.Set(200); std::cout << " => p0.Name() = " << p0.Name() << " ; Value = " << p0.Get() << "\n"; plist[0]->Set(80); std::cout << " => plist[0]->Name() = " << plist[0]->Name() << " plist[0]->Get<int>() = " << plist[0]->Get<int>() << std::endl;
Compilation Output:
$ clang++ property-type-erasure2.cpp -o property-type-erasure2.bin -std=c++1z -g -O0 -Wall $ ./property-type-erasure2.bin ======= Experiment 1 ======= => p[0] = Property{ Name = "basis-points" ; Value = 100 } => p[1] = Property{ Name = "price" ; Value = 5.6 } => p[2] = Property{ Name = "product" ; Value = } ======= Experiment 2 ======= [TRACE] Property [basis-points] set to value = 200 => p0.Name() = basis-points ; Value = 200 [TRACE] Property [basis-points] set to value = 80 => plist[0]->Name() = basis-points plist[0]->Get<int>() = 80
2.10 Virtual Copy Constructor Idiom - Prototype Pattern
2.10.1 Overview
The virtual copy constructor idiom is a workround for allowing a client to clone or performing deep copy at runtime of C++ instances of derived classes from a given base class without knowing the type of those of those instances. In other words, this idioms makes possible to a client to clone instances of derived classes given as pointers to base class (the type of pointers is Base*).
Note: this idiom is quite similar tot the prototype design pattern.
2.10.2 Example: Simple Virtual Copy Constructor
File: file:src/design-patterns/virtual-constructor1.cpp
Note: This example can be improved using smart pointers.
- Interface class:
class IBase{ public: // Destructor always virtual to avoid memory leak, virtual ~IBase() = default; // "virtual copy constructor" virtual auto clone() const -> IBase* = 0; // "virtual default constructor" virtual auto create() const -> IBase* = 0; virtual auto getID() const -> std::string = 0; virtual auto setID(std::string id) -> void = 0; virtual auto show() const -> void = 0; };
- Concrete class DerivedA:
class DerivedA: public IBase{ private: std::string _id; public: DerivedA() : _id("unnamed-A"){ } DerivedA(std::string id) : _id{std::move(id)}{ } auto clone() const -> IBase* { // Invoke copy constructor return new DerivedA(*this); } auto create() const -> IBase* { // Invoke default constructor return new DerivedA(); } auto getID() const -> std::string { return _id; } auto setID(std::string id) -> void { _id = id; } auto show() const -> void { std::cout << " => Class DerivedA - id = " << _id << "\n"; } };
- Concrete clas DerivedB:
class DerivedB: public IBase{ private: std::string _id; public: DerivedB() : _id("unnamed-B"){ } DerivedB(std::string id) : _id{std::move(id)}{ } auto clone() const -> IBase* { // Invoke copy constructor return new DerivedB(*this); } auto create() const -> IBase* { // Invoke default constructor return new DerivedB(); } auto getID() const -> std::string { return _id; } auto setID(std::string id) -> void { _id = std::move(id); } auto show() const -> void { std::cout << " => Class DerivedB - id = " << _id << "\n"; } };
Testing in ROOT REPL:
- Create test objects:
// Load file as a script >> .L virtual-constructor1.cpp >> DerivedA da("objectA"); >> DerivedB db("objectB"); >> da.show() => Class DerivedA - id = objectA >> >> db.show() => Class DerivedB - id = objectB >> >> IBase* ptr = nullptr; >> ptr->getID() (std::string) "objectA" >> ptr->show() => Class DerivedA - id = objectA >>
Clone object DerivedA:
>> IBase* clone1 = ptr->clone(); >> clone1->getID() (std::string) "objectA" >> clone1->show() => Class DerivedA - id = objectA >> if(clone1 != ptr) std::puts("Objects are not the same. OK."); Objects are not the same. OK. >> clone1->setID("object-A-Clone"); >> clone1->getID() (std::string) "object-A-Clone" >> ptr->getID() (std::string) "objectA" >> // Delete cloned object >> delete clone1; >> clone1 = nullptr;
Invoke default constructor of class DerivedB through the object DerivedB:
>> ptr = &db; >> ptr = &db; >> ptr->show() => Class DerivedB - id = objectB >> IBase* clone2 = ptr->create(); >> clone2->show() => Class DerivedB - id = unnamed-B >>
Further Reading:
- ISO C++ - FAQ - Inheritance — What is a virtual constructor
- What is a "virtual constructor"?, C++ FAQ
- Inheritance — <code>virtual</code> functions, C++ FAQ
- More C++ Idioms/Covariant Return Types - Wikibooks, open books for an open world
- Covariant, Templatized Virtual Copy Constructors – Nerdland
- The Anatomy of the Assignment Operator
Codes using "virtual copy constructor":
2.10.3 Example: Virtual Copy Constructor with CRTP
This alternative implementation uses CRTP (Curious Recurring Template) for eliminating the boilerplate code and smart pointers.
File: file:src/design-patterns/virtual-constructor2.cpp
- IBase interface class:
class IBase{ public: // Destructor always virtual to avoid memory leak, virtual ~IBase() = default; // "virtual copy constructor" virtual auto clone() const -> std::unique_ptr<IBase> = 0; // "virtual default constructor" virtual auto create() const -> std::unique_ptr<IBase> = 0; virtual auto getID() const -> std::string = 0; virtual auto setID(std::string id) -> void = 0; virtual auto show() const -> void = 0; };
- CRTP base class:
/** Remember: Template always in header files. */ template<typename Base, typename Derived> class Copyable: public Base{ private: inline auto self() const -> const Derived& { return *static_cast<const Derived* const>(this); } public: virtual ~Copyable() = default; auto clone() const -> std::unique_ptr<Base> { // Invoke copy constructor return std::make_unique<Derived>(this->self()); } auto create() const -> std::unique_ptr<Base> { // Invoke default constructor return std::make_unique<Derived>(); } };
- Class DerivedA. The class DerivedB is similar to this one.
class DerivedA: public Copyable<IBase, DerivedA>{ private: std::string _id; public: DerivedA() : _id("unnamed-A"){ } DerivedA(std::string id) : _id{std::move(id)}{ } auto getID() const -> std::string { return _id; } auto setID(std::string id) -> void { _id = id; } auto show() const -> void { std::cout << " => Class DerivedA - id = " << _id << "\n"; } };
Testign in ROOT REPL:
>> .L virtual-constructor2.cpp >> DerivedA da("objA"); >> DerivedB db("objB"); >> >> IBase* ptr = &da; >> ptr->getID() (std::string) "objA" >> ptr->show() => Class DerivedA - id = objA >> >> auto clone1 = ptr->clone() (std::unique_ptr<IBase, std::default_delete<IBase> > &) @0x7fdf62fa5068 >> >> clone1->setID("objA-clone"); >> clone1->getID() (std::string) "objA-clone" >> >> ptr->getID() (std::string) "objA" >>
2.11 QT Parent-child memory management
QT does not uses smart pointers for widgets memory management, instead it uses raw pointers and parent-child relationship for organizing widgets as tree data structure. In the parent-child relationship memory management technique, every child object (heap-allocated) is owned by a single parent and every parent is responsible for releasing the memory allocated for its children object. When a parent object is destroyed, all its children are also destroyed. The root widget object, which is often stack allocated, calls the destructor of its children objects, and each of its children objects also call the destructors of its offspring.
Note: This is similar to the composite design pattern.
References:
Sample Code
File: parent-child.cpp
#include <iostream> #include <string> #include <vector> class Widget { public: virtual ~Widget() = default; Widget() = default; // Make class non-copiable. Widget(const Widget&) = delete; Widget& operator=(const Widget&) = delete; virtual const char* name() const = 0; virtual void display() const = 0; }; // Convenience function for CRTP pattern. template<typename Dest, typename Src> inline Dest& rcast(Src* td){ return *reinterpret_cast<Dest*>(td); } // Convenience templated function for CRTP pattern. This overload can be // called from member functions functions with const annotation. Such as: // ReturType MyClass::someMemberFunc(arg0, ...) const template<typename Dest, typename Src> inline const Dest& rcast(const Src* td){ return *reinterpret_cast<const Dest*>(td); } template<typename Impl> class Container: public Widget { std::vector<Widget*> m_children{}; public: Container() = default; void addChild(Widget* child) { m_children.push_back(child); } void display() const override { std::fprintf( stdout, " [TRACE] Display container (id = %d) / type = '%s' \n" , rcast<Impl>(this).getid() , name()); for(auto ch: m_children){ ch->display(); } } // Delete all child widget; ~Container() { for(auto ch: m_children){ delete ch; } // CRTP is used because it is not possible to call // virtual method from destructor. std::printf( " [TRACE] Delete container (id = %d) (type = %s) \n" , reinterpret_cast<Impl*>(this)->getid() , rcast<Impl>(this).type() ); } }; class VerticalContainer: public Container<VerticalContainer> { int m_id = 0; public: VerticalContainer() = default; VerticalContainer(int id) { m_id = id; } int getid() const { return m_id; } const char* name() const { return "vcontainer"; } const char* type() const { return "vcontainer"; } }; class HorizontalContainer: public Container<HorizontalContainer> { int m_id = 0; public: HorizontalContainer() = default; HorizontalContainer(int id) { m_id = id; } int getid() const { return m_id; } const char* name() const { return "hcontainer"; } const char* type() const { return "hcontainer"; } }; class Label: public Widget { int m_id = -1; public: Label(int id){ m_id = id; } const char* name() const { return "label"; } int getid() const { return m_id; } void display() const { std::printf(" [TRACE] Display label (n = %d) \n", m_id); } ~Label(){ std::printf(" [TRACE] Label deleted (n = %d) \n", m_id); } }; class Button: public Widget { int m_id = -1; public: Button(){ m_id = 0; } Button(int id){ m_id = id; } const char* name() const { return "button"; } void display() const { printf(" [TRACE] Display button (id = %d) \n", m_id); } ~Button(){ printf(" [TRACE] Button deleted (n = %d) \n", m_id); } }; int main() { // Root object (allocated on stack) // will own all sub-objects. VerticalContainer vbox{0}; vbox.addChild( new Label(15) ); auto hbox = new HorizontalContainer{0}; hbox->addChild( new Button(0) ); hbox->addChild( new Button(1) ); // vbox2 will own the label objects with IDs (100 and 200). auto vbox2 = new VerticalContainer{1}; vbox2->addChild( new Label(100)); vbox2->addChild( new Label(200)); hbox->addChild(vbox2); vbox.addChild( hbox ); vbox.addChild( new Button(2) ); vbox.addChild( new Label(17) ); vbox.display(); printf("\n =========== [TERMINATED] ================\n"); return 0; }
Building and running:
$ clang++ parent-child.cpp -o out.bin -std=c++1z -Wall -Wextra -g $ ./out.bin [TRACE] Display container (id = 0) / type = 'vcontainer' [TRACE] Display label (n = 15) [TRACE] Display container (id = 0) / type = 'hcontainer' [TRACE] Display button (id = 0) [TRACE] Display button (id = 1) [TRACE] Display container (id = 1) / type = 'vcontainer' [TRACE] Display label (n = 100) [TRACE] Display label (n = 200) [TRACE] Display button (id = 2) [TRACE] Display label (n = 17) =========== [TERMINATED] ================ [TRACE] Label deleted (n = 15) [TRACE] Button deleted (n = 0) [TRACE] Button deleted (n = 1) [TRACE] Label deleted (n = 100) [TRACE] Label deleted (n = 200) [TRACE] Delete container (id = 1) (type = vcontainer) [TRACE] Delete container (id = 0) (type = hcontainer) [TRACE] Button deleted (n = 2) [TRACE] Label deleted (n = 17) [TRACE] Delete container (id = 0) (type = vcontainer)
2.12 Cross platform code with conditional compilation
Note: It is not a design pattern, but a methodology for developing cross platform applications in C++.
- Avoid relying on platform-specific features as much as possible.
- Isolate platform-specific code from non-platform specific code using macros, conditional compilation or separate classes.
- Before creating a cross-platform wrapper, check whether there is already some available library that wraps the APIs of the target platforms. For instance, QT framework or WxWidgets GUI library already wraps the UI user interface APIs of many operating systems, including Windows' Win32 API, MacOSX's Cocoa and Linux's Xorg/X11 X Windows Systems. Those libraries save users from reinventing the wheel and ensure cross-platform portability.
- Avoid using platform-specific building systems such as GNU Make, BSD Make, Windows' MSBuild and so on. Instead, it is better to use building systems such as CMake that are widely supported on several operating systems and IDEs.
- Use fixed width integers <csdtint> header: std::int8_t,
std::uint8_t, std::int32_t, std::int64_t … for code related to
serialization, network protocols and device drivers or embedded
systems. The byte sizes of the types int, short, long long long
are not guaranteed to be the same accross all platforms,
operating systems and processor architectures. For instance, a C
or C++ application, built on Windows 64 bits machine, that
attempts to read a value with type long from a file created on a
Linux machine will read the data in incorrect way and may even
corrupting the data, if the application attempts to update the
value.
- Note: Another issue that should be taken into consideration in serialization and network-related code is the platform endianess which is the order which the bytes of numeric types are placed in the memory.
- Jenkins CI for simultaneous and automate cross-platform compilation
- Continuous Integration server can be used for cross-platform compilation and integration testing. Jenkins has the master-slave(agent) architecture where the master coordinates and agents executes the builds in different machines or platforms. This approach allows automatic simultaneous buildings of an application for multiple operating systems, such as Windows, Linux and OSX by running a Jenkins slave/agent node in each operating system where the software will be built.
- Summary:
- Jenkins can be used for building native code artifacts for many platforms at the same time. A jenkins node (agent) running Linux can build Linux ELF executables; a jenkins node running on Windows, can build Windows *.exe, and *.dll artifacts and another node on MacOSX can build artifacts for MacOSX, such as *.dylib and Mach-O executables.
- See:
- Multi-platforms UE4 game on Jenkins | MICHAEL DELVA
- Confluence Mobile - Jenkins Wiki
- Jenkins: Configuring a Linux Slave Node – Embedded Artistry
- Distributed builds - Jenkins - Jenkins Wiki
- Automating cross platform building and continuous testing with Jenkins - Bugsee
- Jenkins Configuration- Windows as Master and Linux as Slave | SAP Blogs
- build - How do we do releases involving multiple platforms using Jenkins? - Stack Overflow
Use macros for isolating platform-specific code
Macros are useful for writing cross-platform code by hiding and isolating platform-specific details, such as specific hardware registers address, compiler extensions, APIs and system calls. This example shows how to use conditional compilation to write code which can be compiled on an Unix-like operating system, such as Linux or OSX and Windows.
Example:
- Include operating-system specific headers:
#if defined(__unix__) // Unix: Linux, BSD, OSX ... #include <unistd.h> #include <dlfcn.h> #include <elf.h> #elif defined(_WIN32) // Windows #include <windows.h> #else #error "Error: Operating system not supported." #endif
- Scoped enum is used to identify the operating system the code was compiled.
enum class SystemType{ WindowsNT, Linux, MacOSX, FreeBSD, Unknown };
- Functions returns operating system that the library was compiled against.
auto getSystemType() -> SystemType { #ifdef __apple__ return SystemType::MacOSX; #elif defined __linux__ return SystemType::Linux; #elif defined _WIN32 || defined _WIN64 return SystemType::WindowsNT; #else return SystemType::Unknown; #endif }
- Function that turns Enum into string:
auto getOperatingSystem() -> std::string { SystemType type = getSystemType(); if (type == SystemType::Linux) return "Linux"; if(type == SystemType::MacOSX) return "MacOSX"; if(type == SystemType::WindowsNT) return "Windows NT"; if(type == SystemType::FreeBSD) return "FreeBSD"; return "Unknown operating system"; }
- Predicate functions for detecting current OS:
auto isWindows() -> bool { return getSystemType() == SystemType::WindowsNT; } // Check whether is U-N-I-X like auto isNixLike() -> bool { auto t = getSystemType(); return t == SystemType::Linux || t == SystemType::FreeBSD || t == SystemType::MacOSX; }
- Operating-system specific APIs are isolated using macros. mkdir => Unix only and CreateDirectory is a Windows API (Win32 API).
/** Cross platform code for creating directory */ void makeDirectory(std::string path){ #if defined __linux__ || defined __apple__ /** ==== U-NIX Specific code ==== */ mkdir(path.c_str(), 0777); #elif _WIN32 /** ==== Windows Specific Code ==== */ CreateDirectoryA(path.c_str(), NULL); #endif }
- Get home or user directory:
/** Get home directory, ~/ or $HOME on Unix or %USERPROFILE% * environment variable on Windows */ std::string getHomeDir(){ if (getSystemType() == SystemType::WindowsNT) return getEnv("USERPROFILE"); else return getEnv("HOME"); }
3 Misc Techniques
3.1 Polymorphic IO
This technique allows to design input or output functions which can read from any type of input stream or print to any type of output stream. Besides C++, this design approach can also be used with any object oriented language with an IO class hierarchy where there are an root abstract input class (in C++ std::istream C++) and a root abstract output class (std::ostream in C++).
File: file:src/design-patterns/polymorphic-io1.cpp
Polymorphic IO Functions:
- The class std::ostream is the base class of all C++'s output streams. By passing the object std::cout to std::ostream, the function writeVector writes the vector to standard output. If the parameter passed is std::sstringstream, the function writes the vector to the string stream.
- The function readVector can read a vector of doubles from any input stream since the class std::istream is the base class of all input streams, as a result, it can read vector from std::cin (console or stdin), file stream std::ifstream, string stream and so on.
- Note: This approach can also be used for binary IO and serialization.
- Summary:
- In order to design an output agnostic function, write to the abstract output std::ostream, instead of writing to concrete outputs such as std::cout, std::cerr and etc.
- To design an input agnostic function, use std::istream instead of a concrete input stream.
- Documentation:
namespace VectorIO{ auto writeVector(std::ostream& os, const std::vector<double>& xs) -> void { os << "VECTOR"; os << " "; for(auto x: xs) os << x << " "; os << "\n"; os.flush(); } auto writeVector(std::ostream&& os, const std::vector<double>& xs) -> void { std::cerr << " [LOG] (writeVector) R-value reference" << "\n"; writeVector(os, xs); } auto readVector(std::istream& is) -> std::vector<double> { std::vector<double> xlist; std::string label; is >> label; if(label != "VECTOR") throw std::runtime_error("Error: wrong file layout."); double x; while(is.good() && !is.eof()){ is >> x; xlist.push_back(x); } return xlist; } auto readVector(std::istream&& is) -> std::vector<double> { std::cerr << " [LOG] (readVector) R-value reference" << "\n"; return readVector(is); } }
Main function:
using VectorIO::writeVector; using VectorIO::readVector; std::vector<double> vtest = {2.0, 4.0, 5.0, 10.0, 3.45, 9.5}; std::cout << "\n TEST1 Writing to stdout (Standard output stream) " << "\n"; writeVector(std::cout, vtest); std::cout << "\n TEST2 Writing to stderr (Standard error stream) " << "\n"; writeVector(std::cerr, vtest); std::cout << "\n TEST3 Writing to std::stringstream " << "\n"; std::stringstream fakeFile; writeVector(fakeFile, vtest); std::cout << "fakeFile = " << fakeFile.str() << "\n"; std::cout << "\n TEST4 Reading from std::stringstream " << "\n"; auto out1 = readVector(fakeFile); writeVector(std::cout, out1); std::cout << "\n TEST5 Write to a file" << "\n"; // Note: It is not possible without R-value reference. writeVector(std::ofstream("vector.txt"), vtest); std::cout << "\n TEST6 Read from file - Version 1" << "\n"; // Call R-value reference version auto out2 = readVector(std::ifstream("vector.txt")); writeVector(std::cout, out2); std::cout << "\n TEST7 Read from file - Version 2" << "\n"; std::ifstream fd{"vector.txt"}; // Call L-value reference version of readVector auto out3 = readVector(fd); writeVector(std::cout, out3);
Output:
$ clang++ polymorphic-io1.cpp -o polymorphic-io1.bin -g -std=c++11 -Wall -Wextra $ ./polymorphic-io1.bin TEST1 Writing to stdout (Standard output stream) VECTOR 2 4 5 10 3.45 9.5 TEST2 Writing to stderr (Standard error stream) VECTOR 2 4 5 10 3.45 9.5 TEST3 Writing to std::stringstream fakeFile = VECTOR 2 4 5 10 3.45 9.5 TEST4 Reading from std::stringstream VECTOR 2 4 5 10 3.45 9.5 9.5 TEST5 Write to a file [LOG] (writeVector) R-value reference TEST6 Read from file - Version 1 [LOG] (readVector) R-value reference VECTOR 2 4 5 10 3.45 9.5 9.5 TEST7 Read from file - Version 2 VECTOR 2 4 5 10 3.45 9.5 9.5
3.2 Multiple booleans encoded as a single bitmask value
Note: It is not a design pattern, but a technique for encoding multiple booleans or flags inside a single value, passing multiple booleans as function parameters or returning multiple booleans as a single value.
Example:
- File: file:src/design-patterns/boolean-bitmask.C (CLING script)
#include <iostream> #include <vector> #include <ostream> // Operator: (<<) #include <string> enum class Permissions: unsigned { executable = 0x01, // decimal = 1 or (1 << 0) writeable = 0x02, // decimal = 2 or (1 << 1) readable = 0x04 // decimal = 4 or (1 << 2) }; Permissions operator | (Permissions lhs, Permissions rhs){ return static_cast<Permissions>(static_cast<unsigned>(lhs) | static_cast<unsigned>(rhs)); } bool operator & (Permissions lhs, Permissions rhs){ return static_cast<unsigned>(lhs) & static_cast<unsigned>(rhs); } // Make permissions enum printable. std::ostream& operator<<(std::ostream& os, const Permissions& p){ os << std::boolalpha; // Make bool printable as 'true' or 'false' instead of 0 or 1 os << "readable = " << (p & Permissions::readable) << "; " << "writeable = " << (p & Permissions::writeable) << "; " << "executable = " << (p & Permissions::executable); return os; }
Running:
// Load script >> .L boolean-bitmask.C >> std::cout << Permissions::readable << std::endl; readable = true; writeable = false; executable = false >> std::cout << Permissions::readable << "\n"; readable = true; writeable = false; executable = false >> std::cout << Permissions::writeable << "\n"; readable = false; writeable = true; executable = false >> std::cout << Permissions::executable << "\n"; readable = false; writeable = false; executable = true >> auto p1 = Permissions::executable | Permissions::readable ; >> std::cout << "p1 => " << p1 << "\n"; p1 => readable = true; writeable = false; executable = true >> >> auto p2 = Permissions::executable | Permissions::readable | Permissions::writeable ; >> std::cout << "p2 => " << p2 << "\n"; p2 => readable = true; writeable = true; executable = true >> >> if(p1 & Permissions::readable) { std::cout << "File is readable" << "\n"; } File is readable >>
Shows all flags or all bits that are set:
void showPermissions(Permissions p){ std::cout << "Is readable? : " << (p & Permissions::readable) << '\n'; std::cout << "Is writeable? : " << (p & Permissions::writeable) << '\n'; std::cout << "Is executable? : " << (p & Permissions::executable) << '\n'; } >> showPermissions(p1) Is readable? : true Is writeable? : false Is executable? : true >> >> showPermissions(p2) Is readable? : true Is writeable? : true Is executable? : true >>
Design 1: Function uses multiple bools creating a file with some permissions.
using FilePerms = std::tuple<bool, bool, bool>; // Design 1: FilePerms createFileDD1( std::string name, bool readable, bool writeable, bool executable ){ std::cout << std::boolalpha << "Create file = " << name << "\n" << " with the following permissions" << "\n" << " + readable = " << readable << "\n" << " + writeable = " << writeable << "\n" << " + executable = " << executable << "\n" << "\n"; return FilePerms {readable, writeable, executable}; } >> auto pd1 = createFileDD1("dataset.txt", true, false, false) Create file = dataset.txt with the following permissions + readable = true + writeable = false + executable = false >> pd1 (std::tuple<bool, bool, bool> &) { true, false, false } >> >> std::cout << "R = " << std::get<0>(pd1) << "\n"; R = true >> std::cout << "W = " << std::get<1>(pd1) << "\n"; W = false >> std::cout << "X = " << std::get<2>(pd1) << "\n"; X = false >>
Design 2: Use bitmask flags instead of bools. The advantage is more readability and less functions parameters.
// Design 1: Permissions createFileDD2(std::string name, Permissions p){ std::cout << std::boolalpha << "Create file = " << name << "\n" << " with the following permissions" << "\n" << " + readable = " << (p & Permissions::readable) << "\n" << " + writeable = " << (p & Permissions::writeable) << "\n" << " + executable = " << (p & Permissions::executable) << "\n" << "\n"; return p; } >> auto pd2 = createFileDD2("Sales-report.xls", Permissions::readable | Permissions::writeable) Create file = Sales-report.xls with the following permissions + readable = true + writeable = true + executable = false (Permissions) : (unsigned int) 6 >> >> createFileDD2("parser.exe", Permissions::readable | Permissions::writeable | Create file = parser.exe with the following permissions + readable = true + writeable = true + executable = true (Permissions) : (unsigned int) 7 >> >> std::cout << "R = " << (pd2 & Permissions::readable) << "\n"; R = true >> std::cout << "W = " << (pd2 & Permissions::writeable) << "\n"; W = true >> std::cout << "X = " << (pd2 & Permissions::executable) << "\n"; X = false
3.3 Universal Polymorphic Factory
This customized implementation of the polymorphic factory can instantiate objects of any type, even if they don't have the same base class. Another feature is that the factory class doesn't need to have any knowledge about any base class. The trick used were templates; void* pointers for type erasure of heap-objects; static objects for registering classes in the factory and RTTI - for generating better error messages and making the conversion from void* to the particular class being instantiated safer.
- File: file:src/design-patterns/factory-universal1.cpp
- Online Compiler: https://rextester.com/GKEBF89553
The class runtime_error_location
class is used for providing exception
with better context information with the line and the file the
exception was generated.
#define RUNTIME_ERROR_LOCATION(message) \ runtime_error_location(__LINE__, __FILE__, message) struct runtime_error_location: public std::exception { std::string message; runtime_error_location(const std::string& text) : message{text} { } runtime_error_location( unsigned int line, const std::string& file, const std::string& text ) { message = file + ":" + std::to_string(line) + ": " + "Error: " + text; } auto what() const noexcept -> const char* { return message.c_str(); } };
The UniversalFactory contains the static factory methods make and makeSafe
which returns an unique_ptr to an object instantiated on the heap. If
the factory method make fails it returns a nullptr without throwing an
exception and makeSafe returns an runtime_error_location
exception if
there is a failure to instantiate.
Once the classes are registered, objects can be instantiated with:
- std::unique_ptr<BaseA> objectD = UniversalFactory::make<BaseClass>("DerivedClassD");
- std::unique_ptr<BaseX> object1 = UniversalFactory::make<BaseClass>("DerivedClassY");
class UniversalFactory{ private: using FactoryMap = std::map<std::string, UniversalFactory*>; // Force global variable to be initialized, thus it avoid the // inialization order fisaco. static auto getRegister() -> FactoryMap& { static FactoryMap classRegister{}; return classRegister; } public: // ========== Instance Methods ========// // Destructor virtual ~UniversalFactory() = default; // Encapsulates class constructor. // The type void* performs pointer type erasure virtual auto create() const -> void* = 0; // Encapsulates destructor virtual auto destroy(void* object) const -> void = 0; virtual auto size() const -> size_t = 0; virtual auto typeinfo() const -> const std::type_info& = 0; // ========== Static Methods ========// /** Register factory object of derived class */ static auto registerFactory(const std::string& name, UniversalFactory* factory) -> void { auto& reg = UniversalFactory::getRegister(); reg[name] = factory; } /** Show all registered classes printing their name to stdout. */ static auto showClasses() -> void { std::cout << " Registered classes. " << "\n"; std::cout << " =================== " << "\n"; for(const auto& pair: UniversalFactory::getRegister()) std::cout << " + " << pair.first << " ; RTTI name = " << pair.second->typeinfo().name() << " ; size (bytes) = " << pair.second->size() << "\n"; } /** Attemp to instantiate a class, if it is not possible, returns nullptr */ template<class BaseClass> static auto make(const std::string& name) -> std::unique_ptr<BaseClass> { FactoryMap& reg = UniversalFactory::getRegister(); auto it = reg.find(name); if(it == reg.end()) return nullptr; // Avoid core dump if the conversion is not possible. if(it->second->typeinfo() != typeid(BaseClass)) return nullptr; void* ptr = it->second->create(); return std::unique_ptr<BaseClass>(reinterpret_cast<BaseClass*>(ptr)); } /** Attempt to instantiate class, if it is not possible throws an exception. */ template<class BaseClass> static auto makeSafe(const std::string& name) -> std::unique_ptr<BaseClass> { // Throw exception for providing better context information and avoid // Core dump due to dangerous reinterpret_cast<>. auto object = UniversalFactory::make<BaseClass>(name); if(object == nullptr) throw RUNTIME_ERROR_LOCATION( std::string("Cannot create type. Failed to cast void* to: ") + name); return object; } }; // -------- End Of class UniversalFactory() ------//
The class FactoryImpl is used for creating static objects (global objects) for registering classes in the factory during the program initialization. This class also performs type erasure of the constructor and destructor by using void* for allowing it to work with any type.
template<typename DerivedClass, typename BaseClass = DerivedClass> class FactoryImpl: UniversalFactory{ private: const std::type_info& _tinfo; public: // Register this global object on the Factory register FactoryImpl(const std::string& name) : _tinfo(typeid(BaseClass)) { std::cerr << " [TRACE] " << " Registered Class = " << name << "\n"; UniversalFactory::registerFactory(name, this); } // Capture class default constructor => constructor type erasure auto create() const -> void* override { return new DerivedClass; } // Capture destructor => Destructor type erasure auto destroy(void* object) const -> void override { delete reinterpret_cast<BaseClass*>(object); } auto typeinfo() const -> const std::type_info& override { return _tinfo; } auto size() const -> size_t override { return sizeof(DerivedClass); } };
Class Registration with static objects.
Classes can be registered in the factory by creating an static instance (static object) of the class FactoryImpl, which registers the class or type passed as template parameter, during the object initialization.
- The first template parameter is the class to be registered and the second one is its base class. If the class to be registered, doesn't have base class, it can omitted. The constructor argument is the name of the class to be registered as string.
- The anonymous namespace in this example is used for defining internal linkage and making the static objects private or only visible in the current compilation unit or file for avoiding name clashes.
- Note: The registered classes doesn't need to have a common base class, thus it is possible to register any class as the class NonDerived in the code below.
namespace { auto register_Base = FactoryImpl<Base, Base>("Base"); auto register_DerivedA = FactoryImpl<DerivedA, Base>("DerivedA"); auto register_DerivedB = FactoryImpl<DerivedB, Base>("DerivedB"); // Classes without a base class doesn't need the second template paramter auto register_NonDerived = FactoryImpl<NonDerived>("NonDerived"); }
The registration boilerplate can be eliminated by using macros:
// Register a non-polymorphic class -> It means a class without base class #define REGISTER_CLASS_TO_FACTORY1(aclass) \ namespace { auto register_ ## derived = FactoryImpl<aclass, aclass>( #aclass ); } // Register a polymorphic class to the factory. #define REGISTER_CLASS_TO_FACTORY2(derivedClass, baseClass) \ namespace { auto register_ ## derivedClass = FactoryImpl<derivedClass, baseClass>( #derivedClass ); } REGISTER_CLASS_TO_FACTORY2(Base, Base); REGISTER_CLASS_TO_FACTORY2(DerivedA, Base); REGISTER_CLASS_TO_FACTORY2(DerivedB, Base); REGISTER_CLASS_TO_FACTORY1(NonDerived);
Usage example in CERN's ROOT/Cling REPL:
Load the proof-of-concept file
>> .L factory-universal1.cpp [TRACE] Registered Class = Base [TRACE] Registered Class = DerivedA [TRACE] Registered Class = DerivedB [TRACE] Registered Class = NonDerived >> >>
Instantiate some classes:
>> auto base = UniversalFactory::make<Base>("Base") (std::unique_ptr<Base, std::default_delete<Base> > &) @0x7f768c558088 >> >> base->getType() (std::string) "Base" >> >> auto derivA = UniversalFactory::make<Base>("DerivedA") (std::unique_ptr<Base, std::default_delete<Base> > &) @0x7f768c5581f8 >> derivA->showType() Class type = DerivedA >> auto derivB = UniversalFactory::make<Base>("DerivedB") (std::unique_ptr<Base, std::default_delete<Base> > &) @0x7f768c558238 >> derivB->showType() Class type = DerivedB >> for(const auto& cls : {"Base", "DerivedA", "DerivedB"} ){ auto obj = UniversalFactory::makeSafe<Base>(cls); std::cout << "Class [" << cls << " ] = " << obj->getType() << " \n"; } // Output: Class [Base ] = Base Class [DerivedA ] = DerivedA Class [DerivedB ] = DerivedB >>
Show all registered classes
>> UniversalFactory::showClasses() Registered classes. =================== + Base ; RTTI name = 4Base ; size (bytes) = 8 + DerivedA ; RTTI name = 4Base ; size (bytes) = 8 + DerivedB ; RTTI name = 4Base ; size (bytes) = 8 + NonDerived ; RTTI name = 10NonDerived ; size (bytes) = 1 >>
Instantiate a non-derived class:
// Instantiate a non derived class of Base >> auto ndc = UniversalFactory::make<NonDerived>("NonDerived") (std::unique_ptr<NonDerived, std::default_delete<NonDerived> > &) @0x7f768c558138 >> >> ndc->printMessage(); ==> I am a non derived class >>
Try creating a class which is not registered
// Returns a null pointer if there is a failure. >> UniversalFactory::make<Base>("Error") == nullptr (bool) true >> UniversalFactory::make<DerivedA>("Error") == nullptr (bool) true >> // makeSafe throws an exception >> auto dummy = UniversalFactory::makeSafe<Base>("NonDerived"); Error in <TRint::HandleTermInput()>: runtime_error_location caught: factory-universal1.cpp:123: Error: Cannot create type. Failed to cast void* to: NonDerived >>
3.4 Visitor Design Pattern for class introspection and serialization
This variation of visitor design pattern presented in this section allows to introspect a class instance, which makes possible to transverse class fields; generate automated human-readable output of member variables and other metadata and also serialize to many different binary or human-readable text formats. Other significant features of this technique is that the classes which performs serialization do not need to know anything about the serializable classes and vice-versa.
File:
Online Compiler:
Features:
- Transverse class fields.
- Generate automated human-readable class description as string.
- An instrospectable class can be serialized to many different formats such as XML, Jon, csv, binary and so on.
- Serialization code is independent from any instrospectable class and can be reused with any other class.
Description
- An instropectable class defines a member function describe which takes a visitor object and describe its fields and metadata to the visitor by calling the visitor methods .name, .id, .description and .field. Note that the class doesn't need to known anything about the visitor.
- Summary: The method describe describes class fields and additional metadata to an visitor object.
struct AClass{ std::string name; int n; double k; long x; AClass(std::string name, int n, double k, long x) : name(name), n(n), k(k), x(x) { } // Delegated constructor AClass(): AClass("unnamed", 10, 25.0, 1000) { } // Note: This method should be in the header file. // Every serializable class implements // a templated member function "describe" // which describe the class structure to // an visitor object. template<class Visitor> void describe(Visitor& v){ v.name("AClass"); v.field(n, "n"); v.field(k, "k"); v.field(x, "x"); v.field(name, "name"); } };
- The visitor class DescriptionVisitor generates and print an automated human-readable description of class member variables.
/** Print class information such as name and fields to stdout. */ struct DescriptionVisitor{ using cstring = const char*; template<class Described> void visit(Described& desc){ desc.describe(*this); } void name(const std::string& className){ std::cout << "Class name = " << className << "\n"; } void field(int& value, cstring name){ std::cout << " Field { name = " << name << " ; type = int " << " ; value = " << value << " }\n"; } void field(long& value, cstring name){ std::cout << " Field { name = " << name << " ; type = long " << " ; value = " << value << " }\n"; } void field(double& value, cstring name){ std::cout << " Field { name = " << name << " ; type = double " << " ; value = " << value << " }\n"; } void field(std::string& value, cstring name){ std::cout << " Field { name = " << name << " ; type = std::string " << " ; value = " << value << " }\n"; } };
- The visitor WriterVisitor serializes to a stream any instrospectable class to human-readable text format.
/** Serialize class data to stream in text format. */ struct WriterVisitor{ using cstring = const char*; std::ostream& _os; WriterVisitor(std::ostream& os): _os(os) { } template<class Described> void visit(Described& desc){ desc.describe(*this); } void name(const std::string& className){ } template<class T> void field(T& x, cstring name){ _os << x << " "; } };
- The visitor ReadVisitor deserializes any instrospectable class from stream.
struct ReadVisitor{ using cstring = const char*; std::istream& _is; ReadVisitor(std::istream& is): _is(is) { } template<class Described> void visit(Described& desc){ desc.describe(*this); } void name(const std::string& className){ } template<class T> void field(T& value, cstring name){ _is >> value; } };
- The visitor SerializeVisitor serializes any instrospectable class to a stream in binary format.
/** Serialize class data to stream in binary format. */ struct SerializeVisitor{ using cstring = const char*; std::ostream& _os; SerializeVisitor(std::ostream& os): _os(os) { } template<class Described> void visit(Described& desc){ desc.describe(*this); } void name(const std::string& className){ } template<class T> void field(T& value, cstring name){ _os.write((char*) &value, sizeof(T)); } void field(std::string& x, cstring name){ //std::cerr << " [LOG] String serializer" << "\n"; auto n = x.size(); _os.write((char*) &n , sizeof(size_t)); _os.write((char*) x.data(), n * sizeof(char)); } };
- The visitor DeserializeVisitor performs the inverse operation of the visitor SerializeVisitor by deserializing any instrospectable class from a stream.
struct DeserializeVisitor{ using cstring = const char*; std::istream& _is; DeserializeVisitor(std::istream& is): _is(is) { } template<class Described> void visit(Described& desc){ desc.describe(*this); } void name(const std::string& className){ } template<class T> void field(T& value, cstring name){ _is.read((char*) &value, sizeof(T)); } void field(std::string& x, cstring name){ size_t n = 0; _is.read((char*) &n, sizeof(size_t)); std::cerr << " [LOG] N = " << n << std::endl; x.resize(n); _is.read((char*) &x[0], n * sizeof(char)); } };
Testing Code:
Compiling and Running:
$ clang++ visitor-instrospection.cpp -o visitor-instrospection.bin -g -std=c++1z -Wall
$ ./visitor-instrospection.bin
Testing object:
AClass cls1("objectA", 200, -2.34, 900);
- Experiment: Print automated human-readable class description to stdout.
std::cout << "\n===== EXPERIMENT 1 ===========" << std::endl; descVisitor.visit(cls1);
Output:
===== EXPERIMENT 1 =========== Class name = AClass Field { name = n ; type = int ; value = 200 } Field { name = k ; type = double ; value = -2.34 } Field { name = x ; type = long ; value = 900 } Field { name = name ; type = std::string ; value = objectA }
- Experiment: Text Serialization/Deserialization
- Note: In addition to stringstream, the writer visitor can be used with any type of stream, including, file stream or output stream std::cout. The std::stringstream was used here for simulating as a file mock object.
auto pseudoFile = std::stringstream{}; auto writer = WriterVisitor(pseudoFile); writer.visit(cls1); auto reader = ReadVisitor(pseudoFile); AClass clsb; reader.visit(clsb); descVisitor.visit(clsb);
Output:
==> pseudoFile = 200 -2.34 900 objectA Class name = AClass Field { name = n ; type = int ; value = 200 } Field { name = k ; type = double ; value = -2.34 } Field { name = x ; type = long ; value = 900 } Field { name = name ; type = std::string ; value = objectA }
- Experiment: Serialize class in binary format to a pseudo-file (std::stringstream)
std::cout << "\n===== EXPERIMENT 5 == Serialize to file ===========" << std::endl; auto mockFile = std::stringstream{}; auto serializer = SerializeVisitor(mockFile); serializer.visit(cls1); std::cout << "Stream = " << stringToHex(mockFile.str()) << std::endl;
Output:
Stream = \xc8\x00\x00\x00\xb8\x1e\x85\xebQ \xb8\x02\xc0\x84\x03\x00\x00\x00\x00\x00\x00\x07\x00\x00\x00\x00\x00\x00\x00o b j e c t A
- Experiment: Deserialize class from stream.
auto deserializer = DeserializeVisitor(mockFile); AClass cls2; deserializer.visit(cls2); descVisitor.visit(cls2);
Output:
===== EXPERIMENT 6 == Deserialize from file =========== [LOG] N = 7 Class name = AClass Field { name = n ; type = int ; value = 200 } Field { name = k ; type = double ; value = -2.34 } Field { name = x ; type = long ; value = 900 } Field { name = name ; type = std::string ; value = objectA }
Remarks and considerations:
- The size of types integers types, namely, int, short and long are platform dependent which can make any serialization code relying on them not portable across different platforms. In order to overcome this problem, it is necessary to use fixed size integers available at header <cstdint>.
- The visitor code request other metadata such as class unique ID, GUID (Global Unique Identifier), field long description and so on.
- It is also possible to define visitor for XML, csv and json serialization.
Inspired by:
- Templatized Visitor Pattern
- cbloom rants: 02-16-13 - The Reflection Visitor Pattern
- Practical C++ RTTI for games | Gamedev Coder Diary
- Static Visitor Reflection in C++ – Swort Elbernux – Medium
- c++ - Is there a better design pattern/method to use? - Stack Overflow
- C++ visitor pattern handling templated string types? - Stack Overflow
- CRTP and dynamic dispatch - General and Gameplay Programming - GameDev.net