This is a follow-up of the Executing tasks based on static polymorphism article, which I recommend to be read for the full picture of what is about to come, as it offers details on why I study this approach and how I implemented it (compile-time iteration of a tuple).
My first attempt on C++ compile-time polymorphism is designed around a task
struct. The requirement for a task is to implement an Execute
method that will perform some work. This requires that the task struct is mine. Otherwise, if there’s some information provided by another library through some struct, I can wrap it in a task that has the required Execute
method.
Inspired by some of Sean Parent’s talks about runtime polymorphism and some of its issues, I found another way of implementing static polymorphism. One that does not have any requirement for the input structs; they don’t need to implement a method nor they must be wrapped in other structs.
Along with the requirements in the previous article, I add these ones:
-
- A container with multiple objects of different types so I have a list of items
- For each object in the container, something different must be performed depending on its type (by iterating all objects, not handling it manually)
- Objects in the container are provided by someone else and cannot be changed
- C++11/14 compatible
This approach starts with some input structs in a tuple (the container); references to the objects constructed of the structs to prevent copies:
namespace input { struct A { int a; }; struct B { int b; }; } using Objects = std::tuple<input::A&, input::A&, input::B&, input::A&>;
Each type needs to trigger a different workflow. I will let the compiler handle the mapping of each type to its workflow by using an overloaded function:
namespace operations { void operate(input::A& a) { a.a *= 10; std::cout << a.a << " - a\n"; } void operate(const input::B& b) { std::cout << b.b << " - b\n"; } }
If I call the operate
function with an object of type A
, the function with the input::A& a
parameter will be called. Similar for type B
, function with parameter input::B& b
.
What is left is an iterator that calls the operate
function on each object in the tuple. I’ve used compile-time recursion to generate all the indices needed to access each element of the tuple. Then I called a function for each object, passing the object as argument.
namespace iterator { template<typename T, std::size_t S = std::tuple_size<T>::value, std::size_t I = S - 1> struct Iterator { template<typename C> void operator()(T& objects, C callback) { callback(std::get<S - I - 1>(objects)); Iterator<T, S, I - 1>{}(objects, callback); } }; template<typename T, std::size_t S> struct Iterator<T, S, 0> { template<typename C> void operator()(T& objects, C callback) { callback(std::get<S - 1>(objects)); } }; }
I placed the above functionalities in different namespaces so I can point out better the fact that they are decoupled.
Now the defined components can be tied together. An iterator instance is created for the container type:
iterator::Iterator<Objects> iterate; // The variable name is a verb (iterate) because the iterator is a functor, thus its instance will be called as a function: iterate().
A container is created with the input objects:
auto a11 = input::A{0}; auto a12 = input::A{1}; auto b11 = input::B{2}; auto a13 = input::A{3}; Objects objects{a11, a12, b11, a13};
The final step is to call the iterator with the objects and a callback. As mentioned earlier, for each type in the tuple I need a different callback, but I can pass only one to the iterator. In C++14, I can use a lambda with an auto
parameter as a proxy between the iterator and the overloads of the operate
function. auto
accepts any type and the appropriate operate
overload is called for the type of the object that has been passed to the proxy.
iterate(objects, [](auto& object) { operations::operate(object); });
C++11 does not allow auto
type parameter for a lambda, so I need an alternative. I put the overloads of operate
into a struct as call operator overloads, transforming that struct into a functor. Then I pass an instance of that struct as a callback to the iterator.
namespace operations { struct operations { void operator()(input::A& a) { a.a *= 10; std::cout << a.a << " - a\n"; } void operator()(const input::B& b) { std::cout << b.b << " - b\n"; } }; } int main() { // ... iterate(objects, operations::operations{}); }
The advantage that I’m satisfied with is having decoupled components. Any type can be placed inside the container, iterated over, and a different workflow followed for each type. All of these without one component knowing about the others.
Something you should be aware of is implicit casts in a particular case: If B
inherits from A
(struct B : A { int b; };
) and there is no operate
overload for B
, the operate
overload for A
is going to be called.
#include <iostream> #include <tuple> namespace input { struct A { int a; }; struct B { int b; }; } namespace operations { void operate(input::A& a) { a.a *= 10; std::cout << a.a << " - a\n"; } void operate(const input::B& b) { std::cout << b.b << " - b\n"; } } namespace iterator { template<typename T, std::size_t S = std::tuple_size<T>::value, std::size_t I = S - 1> struct Iterator { template<typename C> void operator()(T& objects, C callback) { callback(std::get<S - I - 1>(objects)); Iterator<T, S, I - 1>{}(objects, callback); } }; template<typename T, std::size_t S> struct Iterator<T, S, 0> { template<typename C> void operator()(T& objects, C callback) { callback(std::get<S - 1>(objects)); } }; } int main() { using Objects = std::tuple<input::A&, input::A&, input::B&, input::A&>; iterator::Iterator<Objects> iterate; auto a11 = input::A{0}; auto a12 = input::A{1}; auto b11 = input::B{2}; auto a13 = input::A{3}; Objects objects{a11, a12, b11, a13}; iterate(objects, [](auto& object) { operations::operate(object); }); }