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