Reference vs pointer interface#
In the previous tutorial we showed several ways
in which type-erased interfaces can be implemented in tanuki. We also showed
how the interface member functions can be invoked directly from the
wrap
class via the arrow operator ->
:
using foo1_wrap = tanuki::wrap<foo1_iface>;
foo1_wrap w1(foo_model{});
// Invoke the interface's foo() member function
// directly from the wrap class.
w1->foo();
Relying on the arrow operator ->
to access the member functions can however
be problematic.
To begin with, the arrow operator ->
is usually associated with pointers, but, as
we mentioned previously, the wrap
class employs
by default value semantics and not pointer semantics. Although the usage of the arrow operator
with value semantics is not unprecedented (e.g., see
std::optional), it may be a source
of confusion.
Secondly, and more importantly, in generic algorithms and data structures it is often required
to invoke member functions via the dot operator .
, rather than via the arrow operator.
For instance, if we wanted to write the type-erased version of a standard
range, we would need to be able
to access the begin/end functions as .begin()
and .end()
, rather than
->begin()
and ->end()
.
The not-so-good news is that at this time the C++ language offers no clean solution to automatically
enable dot-style access to the member functions of the interface from the wrap
class. This would require
dot operator overloading
or perhaps reflection/metaclasses,
neither of which are currently available.
The better news is that tanuki provides a mechanism to deactivate the pointer interface (i.e., operator ->
)
and activate a reference interface instead (i.e., dot-style access to the member functions). Implementing
a reference interface will require a certain amount of
repetition that can be partially alleviated
via the use of macros.
The following sections explain how to implement reference interfaces. Two APIs can be used: the first one is always available, the second one is a cleaner and less verbose alternative that however requires C++23.
Reference interfaces in C++20#
Consider the simple interface from the previous tutorial, its implementation
and a foo_model
class:
template <typename Base, typename Holder, typename T>
struct foo_iface_impl : public Base {
void foo() const override
{
std::cout << "foo_iface_impl calling foo()\n";
getval<Holder>(this).foo();
}
};
struct foo_iface {
virtual void foo() const = 0;
template <typename Base, typename Holder, typename T>
using impl = foo_iface_impl<Base, Holder, T>;
};
struct foo_model {
void foo() const {}
};
Let us see first a macro-based implementation of a reference interface:
struct foo_ref_iface1 {
template <typename Wrap>
struct impl {
TANUKI_REF_IFACE_MEMFUN(foo)
};
};
A reference interface must be a class which defines in its scope an impl
class
template depending exactly on one parameter, conventionally named Wrap
.
The impl
class is the actual reference interface, and the Wrap
parameter
represents the wrap
class to which the reference interface is applied.
impl
will be used as a static mixin
to augment the wrap
class.
In the body of the impl
class, we should declare and implement the list of member
functions of foo_iface
that we want to be able to access via the dot operator in the
wrap
class. This is most easily accomplished with the help of the
TANUKI_REF_IFACE_MEMFUN
macro, which requires only the name of the member function
(foo
, in this specific case).
In order to understand what is going on behind the scenes, let us also see an example of
an equivalent reference interface which does not use the TANUKI_REF_IFACE_MEMFUN
macro:
struct foo_ref_iface2 {
template <typename Wrap>
struct impl {
void foo() const
{
iface_ptr(*static_cast<const Wrap *>(this))->foo();
}
};
};
Here is what is going on: tanuki makes the wrap
class inherit from foo_ref_iface2::impl
,
so that, via the curiously recurring template pattern (CRTP),
we can invoke the iface_ptr()
function on the wrap
object
to access a pointer to the foo_iface
interface, via which we finally invoke the foo()
member function. Phew!
The TANUKI_REF_IFACE_MEMFUN
macro works by defining several variadic overloads for the
foo()
member function, automatically disabling the invalid ones via
expression SFINAE.
Although TANUKI_REF_IFACE_MEMFUN
is not perfect (e.g., it cannot
be used with certain overloadable operators), you should consider using it if you can
stomach macros.
After the definition of the reference interfaces, we need to configure the wrap
class
to make use of them. This is accomplished by defining custom config
instances and using
them in the wrap
class. For instance, for the macro-based reference interface we
first define a custom configuration called foo_config1
:
inline constexpr auto foo_config1 = tanuki::config<void, foo_ref_iface1>{.pointer_interface = false};
The config
class is templated over two types. Ignoring the first one for the time
being (its meaning will be explained later), the second parameter is the reference interface, which
we set to foo_ref_iface1
to select the macro-based reference interface. We also switch off
the pointer interface in wrap
by setting to false
the pointer_interface
configuration setting – this will ensure that access to the interface
functions via the arrow operator is disabled.
We can now use the custom configuration instance in the definition of the wrap class:
using foo_wrap1 = tanuki::wrap<foo_iface, foo_config1>;
And we can confirm that indeed we can now invoke the foo()
member function via the dot operator:
foo_wrap1 w1(foo_model{});
w1.foo();
foo_iface_impl calling foo()
We can use the non-macro-based foo_ref_iface2
reference interface in exactly the same way.
First, we define a custom configuration instance:
inline constexpr auto foo_config2 = tanuki::config<void, foo_ref_iface2>{.pointer_interface = false};
Second, we can use the custom configuration instance to define another wrap type:
using foo_wrap2 = tanuki::wrap<foo_iface, foo_config2>;
foo_wrap2 w2(foo_model{});
w2.foo();
foo_iface_impl calling foo()
Reference interfaces in C++23#
An alternative API for the definition of reference interfaces is available if your compiler
supports C++23 (more specifically, the so-called
“deducing this”
feature). With the alternative API, it is not necessary any more to define a nested impl
template, and we can define the member function wrappers directly in the body of the
class instead, like this:
struct foo_ref_iface3 {
TANUKI_REF_IFACE_MEMFUN2(foo)
};
Note that, in the C++23 API, we must use the TANUKI_REF_IFACE_MEMFUN2
macro, rather
than TANUKI_REF_IFACE_MEMFUN
.
If you cannot or do not want to use the TANUKI_REF_IFACE_MEMFUN2
macro, here is the implementation
of a C++23 reference interface without macros:
struct foo_ref_iface4 {
template <typename Wrap>
void foo(this const Wrap &self)
{
iface_ptr(self)->foo();
}
};
In other words, the “deducing this” C++23 feature allows us to avoid having to perform the CRTP cast
manually, and we can invoke the iface_ptr()
function directly on the self
/this
argument
instead.
We can then proceed as usual with the definition of the custom configurations using the new reference interfaces:
inline constexpr auto foo_config3 = tanuki::config<void, foo_ref_iface3>{.pointer_interface = false};
inline constexpr auto foo_config4 = tanuki::config<void, foo_ref_iface4>{.pointer_interface = false};
The definition and usage of the wrap
instances is identical to the C++20 API:
using foo_wrap3 = tanuki::wrap<foo_iface, foo_config3>;
foo_wrap3 w3(foo_model{});
w3.foo();
using foo_wrap4 = tanuki::wrap<foo_iface, foo_config4>;
foo_wrap4 w4(foo_model{});
w4.foo();
foo_iface_impl calling foo()
foo_iface_impl calling foo()
Note that the C++20 reference interface API is still available when using C++23 – tanuki will detect whether
or not the nested template impl
is present and will auto-select the API accordingly.
Tips & tricks#
Reference interfaces are useful beyond just enabling dot-style access to the member functions. For instance, they also allow to define nested types/typedefs/aliases and inline friend functions and operators that will be accessible via ADL.
On the other hand, it is also possible to design an API around the wrap
class which does not
employ member functions and which thus needs neither a pointer nor a reference interface. In this usage
mode, the member functions defined in a type-erased interface are used as an implementation detail for free functions,
rather than being invoked directly.
Full code listing#
#include <iostream>
#include <tanuki/tanuki.hpp>
template <typename Base, typename Holder, typename T>
struct foo_iface_impl : public Base {
void foo() const override
{
std::cout << "foo_iface_impl calling foo()\n";
getval<Holder>(this).foo();
}
};
struct foo_iface {
virtual void foo() const = 0;
template <typename Base, typename Holder, typename T>
using impl = foo_iface_impl<Base, Holder, T>;
};
struct foo_model {
void foo() const {}
};
struct foo_ref_iface1 {
template <typename Wrap>
struct impl {
TANUKI_REF_IFACE_MEMFUN(foo)
};
};
struct foo_ref_iface2 {
template <typename Wrap>
struct impl {
void foo() const
{
iface_ptr(*static_cast<const Wrap *>(this))->foo();
}
};
};
inline constexpr auto foo_config1 = tanuki::config<void, foo_ref_iface1>{.pointer_interface = false};
inline constexpr auto foo_config2 = tanuki::config<void, foo_ref_iface2>{.pointer_interface = false};
#if defined(TANUKI_HAVE_EXPLICIT_THIS)
struct foo_ref_iface3 {
TANUKI_REF_IFACE_MEMFUN2(foo)
};
struct foo_ref_iface4 {
template <typename Wrap>
void foo(this const Wrap &self)
{
iface_ptr(self)->foo();
}
};
inline constexpr auto foo_config3 = tanuki::config<void, foo_ref_iface3>{.pointer_interface = false};
inline constexpr auto foo_config4 = tanuki::config<void, foo_ref_iface4>{.pointer_interface = false};
#endif
int main()
{
using foo_wrap1 = tanuki::wrap<foo_iface, foo_config1>;
foo_wrap1 w1(foo_model{});
w1.foo();
using foo_wrap2 = tanuki::wrap<foo_iface, foo_config2>;
foo_wrap2 w2(foo_model{});
w2.foo();
#if defined(TANUKI_HAVE_EXPLICIT_THIS)
using foo_wrap3 = tanuki::wrap<foo_iface, foo_config3>;
foo_wrap3 w3(foo_model{});
w3.foo();
using foo_wrap4 = tanuki::wrap<foo_iface, foo_config4>;
foo_wrap4 w4(foo_model{});
w4.foo();
#endif
}