Hello world#
Consider the shortest possible interface that one can write in traditional C++ object-oriented (OO) style:
struct any_iface {
virtual ~any_iface() = default;
};
Calling any_iface
an interface is quite a stretch, as the only operation
it supports is destruction – but for now this will suffice.
In OO programming, we would then proceed to define implementations
of the any_iface
interface via inheritance:
// An empty implementation.
struct any1 : any_iface {
};
// An implementation storing a string.
struct any2 : any_iface {
std::string str_;
explicit any2(std::string s) : str_(std::move(s)) {}
};
We would then be able to construct instances of any1
and any2
and interact
with them polymorphically via pointers to any_iface
:
// Traditional OO-style.
std::unique_ptr<any_iface> ptr1 = std::make_unique<any1>();
std::unique_ptr<any_iface> ptr2 = std::make_unique<any2>("hello world");
Although there is nothing intrinsically wrong with traditional C++ OO programming, even this short and silly example shows potential drawbacks with this programming style:
it enforces dynamic memory allocation,
it introduces hierarchical coupling (i.e., in order to implement an interface a class has to derive from it),
it enforces pointer semantics.
In recent years, an alternative approach which avoids these drawbacks - and which is known under several monikers such as type-erasure, runtime polymorphism, virtual concepts, traits, etc. - has become increasingly popular. Examples of type-erased classes in the C++ standard library include std::any, std::function and std::thread.
Let us see how tanuki can be used to implement a type-erased analogue to the any_iface
interface. First, we define a generic implementation of the interface:
template <typename Base, typename Holder, typename T>
struct any_iface_impl : public Base {
};
Let us ignore for the moment the Holder
and T
template parameters (their meaning will be
explained later), and note how an implementation must always derive from its Base
.
Behind the scenes, tanuki will ensure that Base
derives from any_iface
.
Aside from the additional funky template arguments and from the fact that it does not derive
directly from any_iface
, any_iface_impl
looks very similar to a traditional
OO programming interface implementation.
Second, we add to the any_iface
definition an impl
template alias to indicate that
any_iface_impl
is the interface implementation:
struct any_iface {
virtual ~any_iface() = default;
template <typename Base, typename Holder, typename T>
using impl = any_iface_impl<Base, Holder, T>;
};
Note
tanuki interfaces do not need to have a virtual
destructor, because tanuki
never deletes through pointers to the interfaces. In this initial example, we leave
the virtual
in order to highlight that it is possible to adapt existing OO interfaces
to work with tanuki, but in the following tutorials we will never declare virtual
destructors.
Note that this is an intrusive way of specifying the implementation of an interface. A non-intrusive alternative is also available, so that it is possible to provide implementations for existing interfaces without modifying them.
And we are done! We can now use any_iface
in the definition of a type-erased
wrap
that can store any destructible object:
// Type-erasure approach.
using any_wrap = tanuki::wrap<any_iface>;
// Store an integer.
any_wrap w1(42);
// Store a string.
any_wrap w2(std::string("hello world"));
// Store anything...
struct foo {
};
any_wrap w3(foo{});
Although this code looks superficially similar to the OO-style approach, there are a few key differences:
no dynamic memory allocation is enforced: the
wrap
class employs an optimisation that stores small values inline;there is no hierarchical coupling: objects of any destructible class can be stored in an
any_wrap
without the need to inherit from anything;any_wrap
employs (by default) value semantics (that is, copy/move/swap operations on awrap
will result in copying/moving/swapping the internal value).
And that’s it for the most minimal example!
Full code listing#
#include <memory>
#include <string>
#include <utility>
#include <tanuki/tanuki.hpp>
template <typename Base, typename Holder, typename T>
struct any_iface_impl : public Base {
};
struct any_iface {
virtual ~any_iface() = default;
template <typename Base, typename Holder, typename T>
using impl = any_iface_impl<Base, Holder, T>;
};
// An empty implementation.
struct any1 : any_iface {
};
// An implementation storing a string.
struct any2 : any_iface {
std::string str_;
explicit any2(std::string s) : str_(std::move(s)) {}
};
int main()
{
// Traditional OO-style.
std::unique_ptr<any_iface> ptr1 = std::make_unique<any1>();
std::unique_ptr<any_iface> ptr2 = std::make_unique<any2>("hello world");
// Type-erasure approach.
using any_wrap = tanuki::wrap<any_iface>;
// Store an integer.
any_wrap w1(42);
// Store a string.
any_wrap w2(std::string("hello world"));
// Store anything...
struct foo {
};
any_wrap w3(foo{});
}