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