A more decoupled approach on static polymorphism

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); });
}

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.