One major advantage of object oriented programming (OOP) languages like C++ is polymorphism. Polymorphism is defined as, “The condition of occurring in several different forms”. In OOP, this would mean the compiler’s ability to infer the right function call / behavior based on the input data alone. For example, when calling an add function the compiler knows how to add two integers or two strings together based on the data type passed in. C++ accomplishes this with two main features, virtual functions / function overrides and template functions and classes. Template functions and classes allow a generic function to be defined that will work with any data type. For example, a templated buffer class could have the ability to create a new buffer of whatever type specified. Virtual functions allow a child class to override a function of a parent class for a more specific implementation. If you want to fully understand how to use virtual functions and exactly how they work under the hood, take a look at my post, Virtual Functions Under the Hood in C++.
Sometimes, there is a need to combine these features and use virtual template functions in C++ but the language does not allow this as the virtual tables would become much too complex. To solve this problem, Andrei Alexandrescu outlined a programming paradigm in his book, “Modern C++ Design“, called policy based design. In this post, I will describe how to use Policy Based Design to simulate virtual template functions in C++.
Why Would we Need a Virtual Template Function?
Let’s explore a realistic example of when a virtual template function would be needed. One good example is serializing data into a buffer for interaction with a serial port. Say we have various data that we would like streamed out of a serial port: a float, an int and a char. A template function is the perfect choice for this type of problem since we can define one generic function that can handle each different data type. Let’s place this into a PlatformInterface class that is intended to interact with all kinds of microcontrollers / systems. This might look as follows:
#ifndef PlatformInterface_hpp #define PlatformInterface_hpp #include <stdio.h> #include <iostream> class PlatformInterface { public: // Template function to serialize any data type into an array template<typename T> static void InsertToBuffer(T data, char* buff) { char* dataBytes = (char*) &data; // Break data into individual bytes size_t dataSize = sizeof(data); for(i = 0; i < dataSize; i++) { buff[i] = dataBytes[i]; std::cout << (uint32_t)buff[i] << std::endl; // Outputs in decimal format } } }; #endif /* PlatformInterface_hpp */
#include <iostream> #include "PlatformInterface" int main(int argc, const char * argv[]) { char buffer[8]; float testFloat = 10.5; // bytes: 0x0, 0x0, 0x28, 0x41 PlatformInterface::InsertToBuffer(testFloat, buffer); std::cout << "Finished" << std::endl; return 0;
Program output:
0 0 40 // = 0x28 65 // = 0x41 Finished
Now this solution will work perfectly fine on a system that has a fundamental char size of 8 bits and is talking to another system of the same endianness. But in software engineering, we always want to stay as generic as possible for code re-use and flexibility. What if we switch systems to a different endianness or a different char size? Some TI processors I have worked with use 16 bit chars and need padding built in to each “byte” in a serial output. This is where it would be extremely useful to have virtual template functions in C++. Then we could define a child class for each system and uniquely define the desired behavior of the insert function while still handling any data type. Since C++ does not allow this implementation, we have to use policy based design to obtain similar results.
What is Policy Based Design:
The main idea behind policy based design is to use specific implementors or “policies” as members of a host class. These policies define all the specific implementations needed or “override” any functions in the host class that would have otherwise been virtual. In this way, we now have a work around to the virtual template function problem in C++. Now, we can define specific policies for any lower level implementations we need, pass those to a host class and have the host class call the desired low level function.
The weirdness of policy based design is in the actual implementation. In order to achieve the functionality defined above, the host class accepts an extra template parameter which acts as the specific policy. Then this template policy is used to call into the specific functions that are in need of overriding in the host class.
The next example shows this paradigm in the context of our PlatformInterface class from before. Let’s say we would like to turn InsertBytes into a pure virtual function so that we can override it for any architecture. And we’d like to have a specific InsertBytes function for an 8 bit, little endian system as well as an 8 bit, big endian system. Using policy based design, that might look something like the following:
#ifndef PlatformInterface_hpp #define PlatformInterface_hpp #include <stdio.h> #include <iostream> template<typename InsertPolicy> class PlatformInterface { public: // Template function to serialize any data type into an array template<typename T> static void InsertToBuffer(T data, char* buff) { InsertPolicy::InsertToBuffer(data, buff); } }; #endif /* PlatformInterface_hpp */
#ifndef LittleEndian8BitImpl_hpp #define LittleEndian8BitImpl_hpp #include <stdio.h> #include <iostream> class LittleEndian8BitImpl { public: // Template function to serialize any data type into an array template<typename T> static void SpecificInsertToBuffer(T data, char* buff) { char* dataBytes = (char*) &data; // Break data into individual bytes size_t dataSize = sizeof(data); for(int i = 0; i < dataSize; i++) { buff[i] = dataBytes[i]; std::cout << (uint32_t)buff[i] << std::endl; // Outputs in decimal format } } }; #endif /* LittleEndian8BitImpl_hpp */
#ifndef BigEndian16BitImpl_hpp #define BigEndian16BitImpl_hpp #include <stdio.h> #include <iostream> class BigEndian8BitImpl { public: // Template function to serialize any data type into an array template<typename T> static void InsertToBuffer(T data, char* buff) { char* dataBytes = (char*) &data; // Break data into individual bytes size_t dataSize = sizeof(data); // Since we're talking to a big endian system, insert bytes in reverse order for(int i = 0; i < dataSize; i++) { buff[i] = dataBytes[dataSize - 1 - i]; std::cout << (uint32_t)buff[i] << std::endl; // Outputs in decimal format } } }; #endif /* BigEndian16BitImpl_hpp */
#include <iostream> #include "PlatformInterface.hpp" #include "LittleEndian8BitImpl.hpp" #include "BigEndian8BitImpl.hpp" main(int argc, const char * argv[]) { char buffer[8]; float testFloat = 10.5; PlatformInterface<BigEndian8BitImpl>::InsertToBuffer<float>(testFloat, buffer); std::cout << "Finished" << std::endl; return 0; }
Program output:
65 // = 0x41 Finished 40 // = 0x28 0 0
How does the correct policy member function get called? Policy based design is what is known as static polymorphism, in other words the compiler figures out the correct function call at compile time instead of dynamically at run time. The compiler looks through any specified policies for member functions with the same signature as the host class call. If it finds a similar enough function in the defined policy, it will substitute in the correct call or throw an error if not. For example, the compiler sees the InsertToBuffer call in the host PlatformInterface class on line 15 and looks through the defined policy for any member functions that return void and take in the specified templated type T and char* as parameters.
Policy based design allows for major flexibility in C++ programming. You can now define as many policies as needed to extend the interface provided by the host class all while still handling templated input types. This means you can specify any behavior you need, for any type input all while keeping code as generic and re-usable as possible. In this way, we now have a work around to virtual template functions in C++. A great quote from Modern C++ Design is, “It is as if the host class were a little code generation engine, and you configure the ways in which it generates code”.
Composition, Inheritance and Enriched Policies
Note that the above example was done using a static singleton implementation. This made the most sense to me for the example of a PlatformInterface class which handles things like serialization. We only need one instance and want the ability to access the InsertToBuffer from anywhere in a project without having to create a new object every time. That being said, obtaining policy based design on a non-static singleton implementation can be achieved in two ways, using composition or inheritance.
Using composition, a private member variable of the Policy type is defined and the dot (.) notation is used when calling the specific policy functions:
#ifndef PlatformInterface_hpp #define PlatformInterface_hpp #include <stdio.h> #include <iostream> template<typename InsertPolicy> class PlatformInterface { public: // Template function to serialize any data type into an array template<typename T> void InsertToBuffer(T data, char* buff) { mPolicy.InsertToBuffer(data, buff); } private: InsertPolicy mPolicy; }; #endif /* PlatformInterface_hpp */
The second way is to use public inheritance and have the host class inherit from the policy class:
#ifndef PlatformInterface_hpp #define PlatformInterface_hpp #include <stdio.h> #include <iostream> template<typename InsertPolicy> class PlatformInterface : public InsertPolicy { public: // Template function to serialize any data type into an array template<typename T> void InsertToBuffer(T data, char* buff) { mPolicy.InsertToBuffer(data, buff); } }; #endif /* PlatformInterface_hpp */
Although the host class inheriting from the policy class seems a little strange, there are a couple advantages. This implementation allows for what is called enriched policies. The basic idea is by using inheritance, the user now has access to the policy member functions through the host class. This is useful if the policy has custom features that need to be configured by the user. The user can define a host object and use it to access the policy in order complete any configuration calls. Then if the user decides to switch to a different policy that does not have these configuration functions, the compiler will throw an error at any point that the old host/policy pair was used. Otherwise, the user would have ensure to update the policy object that was passed to the host object as well as the host object as in the composition implementation. It’s always better if the compiler throws errors as apposed to trusting the user to make all the necessary updates.
Another advantage is size constraints. Due to something called empty base class optimization, if the specified policy class is empty, meaning it has no non-static data members, no virtual functions, no virtual base classes and no non-empty base classes, it will have a size of 0 bytes in the host class. But if we use the composition method and add the empty policy class as a member variable, the host class size will be increased by at least one byte even if it is empty. If a multitude of policies are used in one host class, this could start to cause issues.
One Last Note On Inheritance
If using the inheritance implementation, care needs to be taken when thinking about destruction. Since there is a subclass relationship, the host class could be type casted to a policy and deleted. Unless a virtual destructor is defined in the policy class, this gives unknown behavior. But virtual destructors should be avoided since they would add size overhead. Instead, we should declare a protected, non-virtual (most often empty) destructor for the policy class. This ensures that only derived classes can delete the policy since the destructor cannot be accessed from the outside world.
If you are interested in library design or creating re-usable code, I highly recommend picking up a copy of Modern C++ Design. There are tons easy to follow examples and the concepts are broken down very nicely.
If you found this content interesting or useful, buy me a coffee!
Bitcoin Wallet:
bc1q0vkx34w5y4yt5nq38a4rvk7gvgnxm2xv5lvyft
Shipping container cover photo by: Guillaume Bolduc