Abstraction for better APIs

One problem with the iterator used in the static polymorphism article is the lack of abstraction. The storage container is a tuple and the caller is required to explicitly handle it: create it and pass it to the iterator.

This is not ideal because the iterator exposes internal implementation details and its public interface is coupled to those details. A change of the storage container would break the API and the caller would be required to make changes to their code.

Perhaps the tuple could be switched to an array that uses a variant from boost or C++17 standard library. The implementation change should not impact the public API.

This can be achieved by designing an abstract API that does not expose the internal storage. I’m going to present a way of doing this by wrapping the old static iterator (I’ve changed it a little bit; the entire code is at the end of the article) with a new class that will be the public API.

The API accepts a list of types that you want to iterate over by variadic template arguments.

    template <typename... Ts>
    class StaticIterator {
        static_assert(sizeof...(Ts) > 0, "at least one type is required");
    };

 

For storage, a tuple is used as before, with the types given as template arguments to the class.

    template <typename... Ts>
    class StaticIterator {
        static_assert(sizeof...(Ts) > 0, "at least one type is required");

      private:
        using Objects = std::tuple<Ts...>;
        Objects objects;
    };

 

When constructing the iterator,  the objects that will be iterated over are passed as variadic arguments. A check is performed to see if the given arguments match the given types, to help the caller better understand a possible misuse.

But the way I did this check is not the best because the error message is difficult to get to.  The compiler outputs first other error messages if you pass a list of objects that don’t match the initial types given to the iterator. This is because the initialization of objects member precedes the static assert. It could be an issue to study one day.

    template <typename... Ts>
    class StaticIterator {
        static_assert(sizeof...(Ts) > 0, "at least one type is required");

      public:
        template <typename... Os>
        StaticIterator(Os&&... os) : objects{os...}
        {
            static_assert(std::is_same<std::tuple<Os...>, std::tuple<Ts...>>::value,
                          "argument list must match the types given to the iterator");
        }

      private:
        using Objects = std::tuple<Ts...>;
        Objects objects;
    };

 

Now the class stores the objects and what’s left is a way to iterate over them. The old iterator is embedded and the objects are passed to it by a method (call operator) on the new API.

    template <typename... Ts>
    class StaticIterator {
        static_assert(sizeof...(Ts) > 0, "at least one type is required");

      public:
        template <typename... Os>
        StaticIterator(Os&&... os) : objects{os...}
        {
            static_assert(std::is_same<std::tuple<Os...>, std::tuple<Ts...>>::value,
                          "argument list must match the types given to the iterator");
        }

        template <typename C>
        void operator()(C&& callback)
        {
            iterator(objects, std::forward<C>(callback));
        }

      private:
        using Objects = std::tuple<Ts...>;
        Objects objects;
        Iterator<Objects> iterator;
    };

 

Finally, the caller does not need to handle the tuple anymore. The iterator is given the types and the objects, then a callback is passed to the newly created iterator object.

struct A;
struct B;

void operate(A&);
void operate(B&);

auto a11 = A{};
auto a12 = A{};
auto b11 = B{};
auto a13 = A{};

StaticIterator<A&, A&, B&, A&> iterate{a11, a12, b11, a13};
iterate([](auto& object) { operate(object); });

This is C++14 code, but it can be easily adjusted to C++11 as presented in the previous article on this topic.

 

#include <iostream>
#include <tuple>
#include <type_traits>

namespace input {

struct A {
    int a;
};

struct B {
    int b;
};

}  // namespace input

namespace operations {

void operate(input::A& a)
{
    a.a *= 10;
    std::cout << a.a << " - a\n";
}

void operate(input::B& b) { std::cout << b.b << " - b\n"; }

}  // namespace operations

namespace iterator {
template <typename T, std::size_t S = std::tuple_size<T>::value, std::size_t I = S - 1>
class Iterator {
   public:
    template <typename C>
    void operator()(T& objects, C&& callback)
    {
        callback(std::get<S - I - 1>(objects));
        Iterator<T, S, I - 1>{}(objects, std::forward<C>(callback));
    }
};

template <typename T, std::size_t S>
class Iterator<T, S, 0> {
   public:
    template <typename C>
    void operator()(T& objects, C&& callback)
    {
        callback(std::get<S - 1>(objects));
    }
};

template <typename... Ts>
class StaticIterator {
    static_assert(sizeof...(Ts) > 0, "at least one type is required");

   public:
    template <typename... Os>
    StaticIterator(Os&&... os) : objects{os...}
    {
        static_assert(std::is_same<std::tuple<Os...>, std::tuple<Ts...>>::value,
                      "argument list must match the types given to the iterator");
    }

    template <typename C>
    void operator()(C&& callback)
    {
        iterator(objects, std::forward<C>(callback));
    }

   private:
    using Objects = std::tuple<Ts...>;
    Objects objects;
    Iterator<Objects> iterator;
};

}  // namespace iterator

int main()
{
    auto a11 = input::A{0};
    auto a12 = input::A{1};
    auto b11 = input::B{};
    b11.b = 2;
    auto a13 = input::A{3};

    std::cout << "-> " << a12.a << "\n";

    iterator::StaticIterator<input::A&, input::A&, input::B&, input::A&> iterate{a11, a12, b11, a13};
    iterate([](auto& object) { operations::operate(object); });

    std::cout << "-> " << a12.a << "\n";
}

Leave a Reply

Your email address will not be published.

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