Composing interfaces#

tanuki offers the possibility of combining multiple (potentially unrelated) interfaces into a single one. This can be done in (at least) two ways: either via the easy-to-use composite_iface class, or by manually composing interfaces via inheritance.

Automatic composition via composite_iface#

Consider the following two simple interfaces and their implementations:

template <typename Base, typename Holder, typename T>
struct foo_iface_impl : public Base {
    void foo() const final
    {
        getval<Holder>(this).foo();
    }
};

// The foo() interface.
struct foo_iface {
    virtual void foo() const = 0;

    template <typename Base, typename Holder, typename T>
    using impl = foo_iface_impl<Base, Holder, T>;
};

template <typename Base, typename Holder, typename T>
struct bar_iface_impl : public Base {
    void bar() const final
    {
        getval<Holder>(this).bar();
    }
};

// The bar() interface.
struct bar_iface {
    virtual void bar() const = 0;

    template <typename Base, typename Holder, typename T>
    using impl = bar_iface_impl<Base, Holder, T>;
};

The two interfaces contain the foo() and bar() member functions respectively. Their implementations will invoke the foo() and bar() member functions on the type-erased values.

We can combine foo_iface and bar_iface into a single foobar_iface interface via composite_iface:

    // Define a composite interface.
    using foobar_iface = tanuki::composite_iface<foo_iface, bar_iface>;

Next, we define a type-erased wrapper for the composite interface:

    // Define a wrap for the composite interface.
    using wrap_t = tanuki::wrap<foobar_iface>;

We can then introduce a type that satisfies both interfaces:

struct foobar_model {
    void foo() const
    {
        std::cout << "Invoking foobar_model::foo()" << '\n';
    }
    void bar() const
    {
        std::cout << "Invoking foobar_model::bar()" << '\n';
    }
};

And finally wrap it:

    // Wrap an object that satisfies the composite interface.
    wrap_t w1(foobar_model{});

    // Invoke the member functions.
    w1->foo();
    w1->bar();
Invoking foobar_model::foo()
Invoking foobar_model::bar()

composite_iface is convenient and it has the big advantage of being able to compose interfaces which are unrelated to each other. The main drawback of composite_iface is that, because it uses multiple inheritance behind the scenes, it has a memory footprint proportional to the number of interfaces that it wraps. Roughly speaking, each interface in a composite_iface adds the size of a pointer to the memory footprint of wrap.

Full code listing#

#include <iostream>

#include <tanuki/tanuki.hpp>

template <typename Base, typename Holder, typename T>
struct foo_iface_impl : public Base {
    void foo() const final
    {
        getval<Holder>(this).foo();
    }
};

// The foo() interface.
struct foo_iface {
    virtual void foo() const = 0;

    template <typename Base, typename Holder, typename T>
    using impl = foo_iface_impl<Base, Holder, T>;
};

template <typename Base, typename Holder, typename T>
struct bar_iface_impl : public Base {
    void bar() const final
    {
        getval<Holder>(this).bar();
    }
};

// The bar() interface.
struct bar_iface {
    virtual void bar() const = 0;

    template <typename Base, typename Holder, typename T>
    using impl = bar_iface_impl<Base, Holder, T>;
};

struct foobar_model {
    void foo() const
    {
        std::cout << "Invoking foobar_model::foo()" << '\n';
    }
    void bar() const
    {
        std::cout << "Invoking foobar_model::bar()" << '\n';
    }
};

int main()
{
    // Define a composite interface.
    using foobar_iface = tanuki::composite_iface<foo_iface, bar_iface>;

    // Define a wrap for the composite interface.
    using wrap_t = tanuki::wrap<foobar_iface>;

    // Wrap an object that satisfies the composite interface.
    wrap_t w1(foobar_model{});

    // Invoke the member functions.
    w1->foo();
    w1->bar();
}

Manual composition via inheritance#

If you want to avoid the memory overhead of composite_iface, it is possible to create composite interfaces via inheritance. This approach requires a bit more work and it will introduce coupling between the interfaces. On the other hand, an inheritance-based approach can be appropriate when modelling types which naturally fit in a hierarchy (e.g., standard iterators).

We begin with a foo_iface interface (and its implementation) identical to the previous example:

template <typename Base, typename Holder, typename T>
struct foo_iface_impl : public Base {
    void foo() const final
    {
        getval<Holder>(this).foo();
    }
};

// The foo() interface.
struct foo_iface {
    virtual void foo() const = 0;

    template <typename Base, typename Holder, typename T>
    using impl = foo_iface_impl<Base, Holder, T>;
};

The bar_iface interface and its implementation, however, are now different:

template <typename Base, typename Holder, typename T>
struct bar_iface_impl : foo_iface_impl<Base, Holder, T> {
    void bar() const final
    {
        getval<Holder>(this).bar();
    }
};

// The bar() interface.
struct bar_iface : foo_iface {
    virtual void bar() const = 0;

    template <typename Base, typename Holder, typename T>
    using impl = bar_iface_impl<Base, Holder, T>;
};

bar_iface and its implementation now inherit from foo_iface and its implementation respectively. In other words, bar_iface now plays the role of a composite interface. Let us wrap it and show its usage with the foobar_model class from the previous example:

struct foobar_model {
    void foo() const
    {
        std::cout << "Invoking foobar_model::foo()" << '\n';
    };
    void bar() const
    {
        std::cout << "Invoking foobar_model::bar()" << '\n';
    };
};

int main()
{
    // Define a wrap for the composite interface.
    using wrap_t = tanuki::wrap<bar_iface>;

    // Wrap an object that satisfies the composite interface.
    wrap_t w1(foobar_model{});

    // Invoke the member functions.
    w1->foo();
    w1->bar();
}
Invoking foobar_model::foo()
Invoking foobar_model::bar()

By manually defining a hierarchy of single inheritances, we have now avoided the size overhead of multiple inheritance.

Full code listing#

#include <iostream>

#include <tanuki/tanuki.hpp>

template <typename Base, typename Holder, typename T>
struct foo_iface_impl : public Base {
    void foo() const final
    {
        getval<Holder>(this).foo();
    }
};

// The foo() interface.
struct foo_iface {
    virtual void foo() const = 0;

    template <typename Base, typename Holder, typename T>
    using impl = foo_iface_impl<Base, Holder, T>;
};

template <typename Base, typename Holder, typename T>
struct bar_iface_impl : foo_iface_impl<Base, Holder, T> {
    void bar() const final
    {
        getval<Holder>(this).bar();
    }
};

// The bar() interface.
struct bar_iface : foo_iface {
    virtual void bar() const = 0;

    template <typename Base, typename Holder, typename T>
    using impl = bar_iface_impl<Base, Holder, T>;
};

struct foobar_model {
    void foo() const
    {
        std::cout << "Invoking foobar_model::foo()" << '\n';
    };
    void bar() const
    {
        std::cout << "Invoking foobar_model::bar()" << '\n';
    };
};

int main()
{
    // Define a wrap for the composite interface.
    using wrap_t = tanuki::wrap<bar_iface>;

    // Wrap an object that satisfies the composite interface.
    wrap_t w1(foobar_model{});

    // Invoke the member functions.
    w1->foo();
    w1->bar();
}