Helper function for creating a handwritten vtable
One common pattern when implementing type-erasure is writing the casts from void
pointers to actual types.
Bypassing the type system is generally a dangerous operation and can cause FUD to those reading (and maintaining) the code (myself included), so it bothered me that for every function I had to repeat a code snippet to bypass the type system again and again.
I’m going to use the following shape_view
class for the rest of the notes:
class shape_view {
const void* data;
double(*area_fun)(const void*);
double(*perimeter_fun)(const void*);
int(*edges_fun)(const void*);
public:
template <typename T>
shape_view(const T& t) :
data(&t),
area_fun( [](const void* ptr) { return reinterpret_cast<const T*>(ptr)->area(); }),
perimeter_fun([](const void* ptr) { return reinterpret_cast<const T*>(ptr)->perimeter(); }),
edges_fun( [](const void* ptr) { return reinterpret_cast<const T*>(ptr)->edges(); })
{}
double area() const { return this->area_fun(data); }
double perimeter() const { return this->perimeter_fun(data); }
int edges() const { return this->edges_fun(data); }
};
std::function
and std::function_view
With std::function
, it is possible to rewrite the code as:
class api_ref{
const void* data;
std::function_ref<double()> area_fun;
std::function_ref<double()> perimeter_fun;
std::function_ref<int()> edges_fun;
public:
template <typename T>
shape_view(const T& t) :
data(&t),
area_fun( [&]() { return t.area(); }),
perimeter_fun([&]() { return t.perimeter(); }),
edges_fun( [&]() { return t.edges(); })
{}
double area() const { return this->area_fun(); }
double perimeter() const { return this->perimeter_fun(); }
int edges() const { return this->edges_fun(); }
};
Unfortunately, std::function
might allocate memory, and even if it has a "small object optimization", its sizeof
is generally bigger than a function pointer.
A better approach, to ensure that there are no allocations, is to use function_ref
. It has a constructor that takes a member function, and through that, it is possible to avoid the cast and the lambda:
class shape_view {
const void* data;
std::function_ref<double(const void*)> area_fun;
std::function_ref<double(const void*)> perimeter_fun;
std::function_ref<int(const void*)> edges_fun;
public:
template <typename T>
shape_view(const T& t) :
data(&t),
area_fun( std::nontype_t<&T::area>(), &t),
perimeter_fun(std::nontype_t<&T::perimeter>(), &t),
edges_fun( std::nontype_t<&T::edges>(), &t)
{}
double area() const { return this->area_fun(); }
double perimeter() const { return this->perimeter_fun(); }
int edges() const { return this->edges_fun(); }
};
Note that in both examples, it is even possible to remove const void* data
, as unused.
This is already nearly optimal, but std::function_ref
is also bigger than a function pointer; the pointer to the data is stored multiple times unnecessarily. Also, the cast is not necessary when using a member function, but it is still necessary for most use cases.
This is good enough in multiple situations, especially considering that most of the repetitive code is not there: no lambda to call the underlying function, and no cast.
But if you want to use a lambda or a free function, then you have to cast from void
pointer again.
Wrap the function cast
My original idea was to write a class that works like std::function_ref
, but that does not store the pointer to the data.
At a certain point, I realized that such a class is just a wrapper around a function pointer, and thus only a conversion function is necessary.
nontype_t
is a class that will be available with std::function_ref
from C++26; it is used to store an object, and use it as a template parameter. This makes it possible to create a lambda that calls another function, without storing a function pointer anywhere.
Backporting nontype_t
is trivial, and the factory function might look strange, as none of the parameters are used, but that’s right:
template <auto V>
struct nontype_t{
explicit nontype_t() = default;
using type = decltype( V );
};
template <auto V>
inline constexpr nontype_t<V> nontype{};
template <typename D, auto fptr, typename R, class... Args>
consteval auto make_fptr_impl(const D*, nontype_t<fptr>, R(*)(const D*,Args...)){
return +[]( const void* ptr, Args... xs ) -> R {
auto obj = reinterpret_cast<const D*>( ptr );
return ( *fptr )(obj, std::forward<Args>( xs )... );
};
}
template <typename D, typename n_fptr>
consteval auto make_fptr(const D* ptr, n_fptr fptr){
return make_fptr_impl(ptr, fptr, (typename function_traits<typename n_fptr::type>::ptr)(nullptr));
}
class shape_view {
const void* data;
double(*area_fun)(const void*);
double(*perimeter_fun)(const void*);
int(*edges_fun)(const void*);
public:
template <typename T>
shape_view(const T& t) :
data(&t),
area_fun( make_fptr(&t, nontype_t<+[](const T* ptr) { return ptr->area(); }>())),
perimeter_fun(make_fptr(&t, nontype_t<+[](const T* ptr) { return ptr->perimeter(); }>())),
edges_fun( make_fptr(&t, nontype_t<+[](const T* ptr) { return ptr->edges(); }>()))
{}
double area() const { return this->area_fun(data); }
double perimeter() const { return this->perimeter_fun(data); }
int edges() const { return this->edges_fun(data); }
};
There are no more casts in shape_view
, they are encapsulated in make_fptr
. In exchange, I needed to use the function_traits
facility to destructure a function, which is a lot of (boilerplate) code.
Unfortunately the code is still harder to read than necessary; especially that nontype_t<+[](
and ; }>()))
for every function pointer member variable.
It also works only if data
is used as the first parameter, but that should not be an issue.
Support for member functions
The code using std::function_ref
is much more concise, thus having make_fptr
to support member function directly is the next logical step.
For completeness, I’ve also added support creating noexcept
and non-const
functions and took advantage of templated lambdas to remove some code duplication:
template <auto V>
struct nontype_t{
explicit nontype_t() = default;
using type = decltype( V );
};
template <auto V>
inline constexpr nontype_t<V> nontype{};
template <typename P, auto fptr>
consteval auto make_fptr(P, nontype_t<fptr>){
static_assert(std::is_pointer_v<P>);
using void_ptr = std::conditional_t<std::is_const_v<std::remove_pointer_t<P>>,
const void*,
void*
>;
using fptr_traits = function_traits<typename nontype_t<fptr>::type>;
if constexpr(std::is_member_function_pointer_v<typename nontype_t<fptr>::type>){
auto l = [] <class R, class... Args>(R(*)(Args...)){
return +[]( void_ptr ptr, Args... xs ) noexcept(fptr_traits::is_noexcept) -> R {
auto obj = reinterpret_cast<P>( ptr );
return ( obj->*fptr )(std::forward<Args>( xs )... );
};
};
return l(typename fptr_traits::ptr());
}else{
auto l = [] <class R, class... Args>(R(*)(P, Args...)){
return +[]( void_ptr ptr, Args... xs ) noexcept(fptr_traits::is_noexcept) -> R {
auto obj = reinterpret_cast<P>( ptr );
return ( *fptr )(obj, std::forward<Args>( xs )... );
};
};
return l(typename fptr_traits::ptr());
}
}
struct square {
double x;
double y;
double area() const;
double perimeter() const;
int edges() const;
};
struct circle {
double radius;
double area() const;
double perimeter() const;
int edges() const;
};
class shape_view {
const void* data;
double(*area_fun)(const void*);
double(*perimeter_fun)(const void*);
int(*edges_fun)(const void*);
public:
template <typename T>
shape_view(const T& t) :
data(&t),
area_fun( make_fptr(&t, nontype_t<&T::area>())),
perimeter_fun(make_fptr(&t, nontype_t<&T::perimeter>())),
edges_fun( make_fptr(&t, nontype_t<&T::edges>()))
{}
double area() const { return this->area_fun(data); }
double perimeter() const { return this->perimeter_fun(data); }
int edges() const { return this->edges_fun(data); }
};
void foo(square& s){
auto sv = shape_view(s);
sv.area();
sv.perimeter();
sv.edges();
}
The complete example can be found at compiler explorer.
Note 📝 | I am aware of std::invoke , but I do not know how to simplify the code of make_fptr without modifying function_traits, which I currently do not want to do. |
The constructor of shape_view
looks much better. There are no repetitive lambdas, no casts in shape_view
, and the code is much more legible than before, and without any overhead (make_fptr
is even consteval
) or additional space requirements.
The main issue of using a member function directly is that function signatures have to match.
For example; a pointer to void(const void*)
is not convertible to a pointer to void(void*)
, although it would be a safe conversion.
The same holds for conversting between return values; a pointer to short(const void*)
is not convertible to int(const void*)
.
It is possible to encapsulate those conversions in lambdas, and a more complex factory function could provide, at least partially, such functionality, more on that later.
Also, note that the first parameter of make_fptr
is unnecessary. I decided to add it as it can help to avoid mismatches between the function signature, and the actual type store in the type-erased class. Better safer than sorry.
Note 📝 | functions with volatile and variadic parameters are not supported by make_fptr . I currently see little value in making make_fptr even more complex. |
Variadic factory
All function pointers are initialized in a similar way, and often using an outline vtable is better than an inline.
Wouldn’t it thus make sense to have a factory function that creates the whole vtable directly?
A possible implementation would look like the following:
template <typename P, class... Args>
consteval auto make_vtable(P ptr, Args... args){
return std::tuple(make_fptr(ptr, args)...);
}
class shape_view {
const void* data;
std::tuple<
double(*)(const void*),
double(*)(const void*),
int(*)(const void*)
> v;
public:
template <typename T>
shape_view(const T& t) :
data(&t),
v(make_vtable(&t, nontype_t<&T::area>(), nontype_t<&T::perimeter>(), nontype_t<&T::edges>()))
{}
double area() const { return std::get<0>(this->vtable)(data); }
double perimeter() const { return std::get<1>(this->vtable)(data); }
int edges() const { return std::get<2>(this->vtable)(data); }
};
If there are multiple function pointers with the same signature, using std::get
with an index makes it easy to use by accident the wrong pointer. A named parameter would be better, thus I would personally prefer a simple struct
:
template <typename E, typename P, class... Args>
consteval auto make_vtable(P ptr, Args... args){
return E{make_fptr(ptr, args)...};
}
struct vtable{
double(*area_fun)(const void*);
double(*perimeter_fun)(const void*);
int(*edges_fun)(const void*);
};
class shape_view {
const void* data;
vtable v;
public:
template <typename T>
shape_view(const T& t) :
data(&t),
vtable(make_vtable<vtable>(&t, nontype_t<&T::area>(), nontype_t<&T::perimeter>(), nontype_t<&T::edges>()))
{}
double area() const { return this->v.area_fun(data); }
double perimeter() const { return this->v.perimeter_fun(data); }
int edges() const { return this->v.edges_fun(data); }
};
I still think the code is not better than the one initializing all member variables explicitly, as the construction of vtable
is too error-prone. Last but not least, compiler errors are much less readable.
Support conversion for return types and input parameters
As mentioned previously, when not using a lambda, the function signatures have to match.
Handling conversion is not too difficult.
Note that, for some reason, in the following example struct square
returns a float
from area
, and not a double
. But since float
is convertible to double
, this should not be an issue, and in fact, when writing a lambda that calls the member function, everything works as expected.
With the following init_fptr
function, the factory takes the target as parameters and uses its properties to do an implicit conversion between return types (or a cast to void
to ignore the return value).
template <typename F, typename P, auto fptr>
constexpr void init_fptr(F*& f, P, nontype_t<fptr>) noexcept {
static_assert(std::is_pointer_v<P>);
using void_ptr = std::conditional_t<std::is_const_v<std::remove_pointer_t<P>>,
const void*,
void*
>;
using fptr_traits = function_traits<typename nontype_t<fptr>::type>;
using fptr_target_traits = function_traits<F>;
if constexpr(std::is_member_function_pointer_v<typename nontype_t<fptr>::type>){
auto l = [] <class R, class... Args>(R(*)(Args...)){
return +[]( void_ptr ptr, Args... xs ) noexcept(fptr_traits::is_noexcept) -> fptr_target_traits::result {
auto obj = reinterpret_cast<P>( ptr );
if constexpr(std::is_void_v<typename fptr_target_traits::result>){
(void) ( obj->*fptr )(std::forward<Args>( xs )... );
} else {
return ( obj->*fptr )(std::forward<Args>( xs )... );
}
};
};
f = l(typename fptr_traits::ptr());
}else{
auto l = [] <class R, class... Args>(R(*)(P, Args...)){
return +[]( void_ptr ptr, Args... xs ) noexcept(fptr_traits::is_noexcept) -> fptr_target_traits::result {
auto obj = reinterpret_cast<P>( ptr );
if constexpr(std::is_void_v<typename fptr_target_traits::result>){
(void) ( *fptr )(obj, std::forward<Args>( xs )... );
} else {
return ( *fptr )(obj, std::forward<Args>( xs )... );
}
};
};
f = l(typename fptr_traits::ptr());
}
}
struct square {
double x;
double y;
float area() const;
double perimeter() const;
int edges() const;
};
class shape_view {
const void* data;
double(*area_fun)(const void*);
double(*perimeter_fun)(const void*);
int(*edges_fun)(const void*);
public:
template <typename T>
shape_view(const T& t) :
data(&t),
perimeter_fun(make_fptr(&t, nontype_t<&T::perimeter>())),
edges_fun( make_fptr(&t, nontype_t<&T::edges>()))
{
init_fptr(area_fun, &t, nontype_t<&T::area>());
}
double area() const { return this->area_fun(data); }
double perimeter() const { return this->perimeter_fun(data); }
int edges() const { return this->edges_fun(data); }
};
The current implementation of init_fptr
works only if an implicit conversion is possible; in this case, float
is implicitly convertible to double
. The code can be changed to accept explicit conversions too, but I fear that it might lead to some surprising behavior.
If one wants to support conversion between the function parameters, one has to use a similar appaoch for the arguments. Use function_traits
on the target function, and since it is always a function pointer, it let us even use std::invoke
and simplify the code:
template <typename F, typename P, auto fptr>
constexpr void init_fptr(F*& f, P, nontype_t<fptr>) noexcept {
static_assert(std::is_pointer_v<P>);
using void_ptr = std::conditional_t<std::is_const_v<std::remove_pointer_t<P>>,
const void*,
void*
>;
using fptr_traits = function_traits<typename nontype_t<fptr>::type>;
using fptr_target_traits = function_traits<F>;
auto l = [] <class R, class... Args>(R(*)(void_ptr, Args...)){
return +[]( void_ptr ptr, Args... xs ) noexcept(fptr_traits::is_noexcept) -> R {
auto obj = reinterpret_cast<P>( ptr );
if constexpr(std::is_void_v<R>){
(void) std::invoke(fptr, obj, std::forward<Args>( xs )... );
} else {
return std::invoke(fptr, obj, std::forward<Args>( xs )... );
}
};
};
f = l(typename fptr_target_traits::ptr());
}
It am sure that it possible to take into consideration explicit conversions for function parameters too; unfortunately, I do not know how to achieve it (yet).
Similarly to return values, I am not convinced that they are desirable.
At the moment, if those conversions are desired, just use lambda. It is much more flexible, does not require a more complex factory function, and compilers provide better error messages.
Do you want to share your opinion? Or is there an error, some parts that are not clear enough?
You can contact me anytime.