Hello world

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 a wrap 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{});
}