A simple interface#

The minimal example shown in the previous tutorial was not very interesting, as the interface we implemented did not define any behaviour other than destruction. In this tutorial, we are going to take the next step and implement a slightly more useful interface consisting of a single member function called foo().

Take 1: The basics#

Here is the first version of our interface:

struct foo1_iface {
    virtual void foo() const = 0;

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

Here is the declaration of a tanuki implementation for this interface:

template <typename Base, typename Holder, typename T>
struct foo1_iface_impl : public Base {

As explained in the previous tutorial, an interface implementation must always derive from its Base template parameter. Additionally, we must provide an implementation for the void foo() const function:

template <typename Base, typename Holder, typename T>
struct foo1_iface_impl : public Base {
    void foo() const override
    {
        std::cout << "foo1_iface_impl calling foo()\n";
        static_cast<const Holder *>(this)->_tanuki_value.foo();
    }
};

The Holder template parameter is a class defined in the tanuki library which stores the value we are type-erasing as the _tanuki_value data member. Holder derives from foo1_iface_impl and thus we can reach the _tanuki_value data member via the cast static_cast<const Holder *>(this) leveraging the curiously recurring template pattern (CRTP). In other words, this implementation of the void foo() const function will invoke the foo() member function of the type-erased value.

We can now proceed to define a class providing such a foo() member function:

struct foo_model {
    void foo() const
    {
        std::cout << "foo_model calling foo()\n";
    }
};

We are now ready to define and use a type-erased wrapper:

    using foo1_wrap = tanuki::wrap<foo1_iface>;

    foo1_wrap w1(foo_model{});
    w1->foo();

This code will produce the following output:

foo1_iface_impl calling foo()
foo_model calling foo()

That is, the foo1_wrap wrapper provides access, via the arrow operator ->, to the foo1_iface_impl::foo() member function, which in turn invokes foo_model::foo().

Take 2: Introducing getval()#

The use of the CRTP in the interface implementation can be a bit verbose and ugly. tanuki provides a couple of helpers, called getval(), which can help reducing typing in an interface implementation. Let us see them in action with a second version of the interface implementation:

template <typename Base, typename Holder, typename T>
struct foo2_iface_impl : public Base {
    void foo() const override
    {
        std::cout << "foo2_iface_impl calling foo()\n";
        getval<Holder>(this).foo();
    }
};

Here we have replaced the explicit static_cast with an invocation of getval(), which, behind the scenes, performs the CRTP downcast and returns a (const) reference to the type-erased value.

Note that getval() does a bit more than providing access to the type-erased value, as it will be explained in a later tutorial. In any case, from now on, all interface implementations shown in these tutorials will make use of getval() in order to reduce typing.

Let us see the new interface implementation in action:

    using foo2_wrap = tanuki::wrap<foo2_iface>;

    foo2_wrap w2(foo_model{});
    w2->foo();
foo2_iface_impl calling foo()
foo_model calling foo()

Take 3: Concept checking#

What happens though if we try to construct a foo2_wrap from an object which does not provide a foo() member function (such as, e.g., an int)? The compiler will loudly complain:

simple_interface.cpp:34:23: error: request for member ‘foo’ in [...] which is of non-class type ‘const int’
34 |         _tanuki_value.foo();

This is of course not ideal, for at least a couple of reasons:

  • the error is generated deep in the bowels of the interface implementation, it would be better if we could catch the problem earlier and provide a more meaningful error message;

  • we might want to provide an alternative implementation of the interface for value types that do not have a foo() member function.

C++20 concepts are a powerful tool that allows us to address both of these issues in more than one way.

As a first approach, we are going to constrain our interface implementation to types that provide a foo() member function. In order to do this, we first introduce a fooable concept:

template <typename T>
concept fooable = requires(const T &x) { x.foo(); };

This concept will be satisfied by all types which provide a const foo() member function. We then constrain the interface implementation to fooable types via the requires keyword:

template <typename Base, typename Holder, typename T>
    requires fooable<T>
struct foo3_iface_impl : public Base {
    void foo() const override
    {
        std::cout << "foo3_iface_impl calling foo()\n";
        getval<Holder>(this).foo();
    }
};

That is, the third template parameter T in a tanuki interface implementation represents the type of the value stored in the Holder class, and we can thus use it to specialise and/or constrain interface implementations. Here is the new interface implementation in action:

    using foo3_wrap = tanuki::wrap<foo3_iface>;

    foo3_wrap w3(foo_model{});
    w3->foo();
foo3_iface_impl calling foo()
foo_model calling foo()

foo3_iface_impl is similar to what Rust calls a blanket implementation.

What happens now if we try to construct a foo3_wrap from an int? The error message given by the compiler changes:

simple_interface.cpp:84:21: error: no matching function for call to ‘tanuki::v1::wrap<foo3_iface>::wrap(int)’
[...]
1073 |         wrap(T &&x) noexcept(noexcept(this->ctor_impl<detail::value_t_from_arg<T &&>>(std::forward<T>(x)))
     |         ^~~~
tanuki.hpp:1073:9: note:   template argument deduction/substitution failed:
tanuki.hpp:1073:9: note: constraints not satisfied

That is, the constructor of the wrap class now detects that int does not have an interface implementation, and as a consequence the compiler detects an error before trying to invoke the (non-existing) foo() member function on the int. We can confirm that the non-constructibility of foo3_wrap from int is detected at compile time by checking the std::is_constructible type trait:

    std::cout << std::boolalpha;
    std::cout << "Is foo3_wrap constructible from an int? " << std::is_constructible_v<foo3_wrap, int> << '\n';
Is foo3_wrap constructible from an int? false

This is all fine and dandy, but what if we wanted to provide an implementation of our interface also for int? We can do this with yet another (and final) variation on the theme.

Take 4: Empty implementations#

First off, we begin with an empty interface implementation:

template <typename Base, typename Holder, typename T>
struct foo4_iface_impl {
};

This is an invalid implementation because it does not derive from Base. Second, we add a constrained specialisation of the interface implementation for fooable types (that is, types providing a const foo() member function):

template <typename Base, typename Holder, typename T>
    requires fooable<T>
struct foo4_iface_impl<Base, Holder, T> : public Base {
    void foo() const override
    {
        std::cout << "foo4_iface_impl calling foo()\n";
        getval<Holder>(this).foo();
    }
};

Finally, we add a specialisation of foo4_iface_impl for int:

template <typename Base, typename Holder>
struct foo4_iface_impl<Base, Holder, int> : public Base {
    void foo() const override
    {
        std::cout << "foo4_iface_impl implementing foo() for the integer " << getval<Holder>(this) << "\n";
    }
};

This implementation will just print to screen the value of the integer when the foo() function is invoked.

Let use see the new interface implementation in action:

    using foo4_wrap = tanuki::wrap<foo4_iface>;

    foo4_wrap w4(foo_model{});
    w4->foo();
    foo4_wrap w4a(42);
    w4a->foo();
foo4_iface_impl calling foo()
foo_model calling foo()
foo4_iface_impl implementing foo() for the integer 42

It works! Bu what happens if we try to construct a foo4_wrap from an object that is neither fooable nor an int? The wrap class will detect that the interface implementation corresponding to an object of such type is empty (i.e., invalid), and it will thus disable the constructor. We can confirm that this is the case by checking the constructibility of foo4_wrap from a float (which is neither fooable nor an int):

    std::cout << "Is foo4_wrap constructible from a float? " << std::is_constructible_v<foo4_wrap, float> << '\n';
Is foo4_wrap constructible from a float? false

And that is enough for now!

Full code listing#

#include <iostream>

#include <tanuki/tanuki.hpp>

template <typename Base, typename Holder, typename T>
struct foo1_iface_impl : public Base {
    void foo() const override
    {
        std::cout << "foo1_iface_impl calling foo()\n";
        static_cast<const Holder *>(this)->_tanuki_value.foo();
    }
};

struct foo1_iface {
    virtual void foo() const = 0;

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

struct foo_model {
    void foo() const
    {
        std::cout << "foo_model calling foo()\n";
    }
};

template <typename Base, typename Holder, typename T>
struct foo2_iface_impl : public Base {
    void foo() const override
    {
        std::cout << "foo2_iface_impl calling foo()\n";
        getval<Holder>(this).foo();
    }
};

struct foo2_iface {
    virtual void foo() const = 0;

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

template <typename T>
concept fooable = requires(const T &x) { x.foo(); };

template <typename Base, typename Holder, typename T>
    requires fooable<T>
struct foo3_iface_impl : public Base {
    void foo() const override
    {
        std::cout << "foo3_iface_impl calling foo()\n";
        getval<Holder>(this).foo();
    }
};

struct foo3_iface {
    virtual void foo() const = 0;

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

template <typename Base, typename Holder, typename T>
struct foo4_iface_impl {
};

template <typename Base, typename Holder, typename T>
    requires fooable<T>
struct foo4_iface_impl<Base, Holder, T> : public Base {
    void foo() const override
    {
        std::cout << "foo4_iface_impl calling foo()\n";
        getval<Holder>(this).foo();
    }
};

template <typename Base, typename Holder>
struct foo4_iface_impl<Base, Holder, int> : public Base {
    void foo() const override
    {
        std::cout << "foo4_iface_impl implementing foo() for the integer " << getval<Holder>(this) << "\n";
    }
};

struct foo4_iface {
    virtual void foo() const = 0;

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

int main()
{
    using foo1_wrap = tanuki::wrap<foo1_iface>;

    foo1_wrap w1(foo_model{});
    w1->foo();

    using foo2_wrap = tanuki::wrap<foo2_iface>;

    foo2_wrap w2(foo_model{});
    w2->foo();

    using foo3_wrap = tanuki::wrap<foo3_iface>;

    foo3_wrap w3(foo_model{});
    w3->foo();

    std::cout << std::boolalpha;
    std::cout << "Is foo3_wrap constructible from an int? " << std::is_constructible_v<foo3_wrap, int> << '\n';

    using foo4_wrap = tanuki::wrap<foo4_iface>;

    foo4_wrap w4(foo_model{});
    w4->foo();
    foo4_wrap w4a(42);
    w4a->foo();

    std::cout << "Is foo4_wrap constructible from a float? " << std::is_constructible_v<foo4_wrap, float> << '\n';
}