Type-erasure for references

Type-erasure for references#

In all the examples we have seen so far, wrap objects were constructed by copying/moving values into a wrap. It is however also possible to construct wrap objects that contain references to existing values, rather than copies of the values. References are stored in a wrap via std::reference_wrapper, and, with small additional effort, it is possible to write interface implementations which work seamlessly with both values and references.

Consider the following interface:

struct foobar_iface {
    virtual void foo() const = 0;
    virtual void bar() = 0;

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

This is similar to the interface considered in an earlier tutorial. We introduce two concepts to check the presence of the foo() and bar() member functions:

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

template <typename T>
concept barable = requires(T &x) { x.bar(); };

And a foobar_model class implementing the foo() and bar() member functions:

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

We then provide an empty default interface implementation:

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

And here is the implementation of the interface for values or references providing foo() and bar() member functions:

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

The only thing that has changed with respect to the original example is that the concept checks fooable and barable are not applied any more to T directly, but rather to unwrap_cvref_t of T. That is, the implementation is enabled not only if T satisfies the fooable and barable concepts, but also if T is a std::reference_wrapper of a type which satisfies these concepts.

Here is what is going on: when a wrap is constructed from a std::reference_wrapper, the getval() functions will automatically unwrap the reference wrapper and return a reference to the referred-to value. This allows to use the same code for the implementation of an interface, regardless of whether a copy or a reference is being stored in the Holder. Let us see a usage example:

    using wrap_t = tanuki::wrap<foobar_iface>;

    // Store the copy of an object.
    wrap_t w1{foobar_model{}};
    w1->foo();

    // Store a reference to an existing object.
    foobar_model f;
    wrap_t w2{std::ref(f)};
    w2->bar();
foobar_iface_impl calling foo()
foobar_model calling foo()
foobar_iface_impl calling bar()
foobar_model calling bar()

Because the wrap w2 is constructed from std::ref(f), it will store a reference to f rather than a copy. We can confirm that this is the case by comparing the address of f to the address of the object contained in w2:

    // Check that the value in w2 points to f.
    std::cout << "f points to              : " << &f << '\n';
    std::cout << "The value in w2 points to: " << &value_ref<std::reference_wrapper<foobar_model>>(w2).get() << '\n';
f points to              : 0x7f7a0a000030
The value in w2 points to: 0x7f7a0a000030

Note that, just like regular references, w2 is a non-owning view of f: it will remain valid as long as the f object is alive, and it is your responsibility to ensure that w2 is not used after the destruction of f.

A caveat about const references#

Storing const references in a wrap object is possible, but it comes with a caveat: if you try to invoke a non-const member function of the interface on a wrap object containing a const reference, then a std::runtime_error exception will be thrown by the getval() accessor:

    // Store a const reference to f.
    wrap_t w3{std::cref(f)};
    // WARNING: this will throw an exception!
    // w3->bar();

In order to prevent runtime errors, you should ensure that a wrap containing a const reference is itself declared as const:

    // Store a const reference to f.
    const wrap_t w4{std::cref(f)};
    // OK: this will not compile.
    // w4->bar();

This way, the error will happen at compile time rather than at runtime.

Full code listing#

#include <functional>
#include <iostream>

#include <tanuki/tanuki.hpp>

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

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

template <typename T>
concept barable = requires(T &x) { x.bar(); };

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

struct foobar_iface {
    virtual void foo() const = 0;
    virtual void bar() = 0;

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

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

int main()
{
    using wrap_t = tanuki::wrap<foobar_iface>;

    // Store the copy of an object.
    wrap_t w1{foobar_model{}};
    w1->foo();

    // Store a reference to an existing object.
    foobar_model f;
    wrap_t w2{std::ref(f)};
    w2->bar();

    // Check that the value in w2 points to f.
    std::cout << "f points to              : " << &f << '\n';
    std::cout << "The value in w2 points to: " << &value_ref<std::reference_wrapper<foobar_model>>(w2).get() << '\n';

    // Store a const reference to f.
    wrap_t w3{std::cref(f)};
    // WARNING: this will throw an exception!
    // w3->bar();

    // Store a const reference to f.
    const wrap_t w4{std::cref(f)};
    // OK: this will not compile.
    // w4->bar();
}