You could type erase the problem. This has the advantage that you can split the execution of the operation from the generation of the list.
template<class T, auto Op, class R, class...Args>
concept opable = requires(T& t, Args&&...args) {
{ Op(t, std::forward<Args>(args)...) }->std::convertible_to<R>;
};
template<auto Op, class Sig=void()>
struct opable_ref;
template<auto Op, class R, class...Args>
struct opable_ref<Op, R(Args...)> {
template<opable<Op, R, Args...> U>
opable_ref( U& u ):
pv(std::addressof(u)),
pf([](void const*pv, Args&&...args)->R{
return Op( *static_cast<U*>(const_cast<void*>(pv)), std::forward<Args>(args)... );
})
{}
R operator()(Args...args)const{
return pf(pv, std::forward<Args>(args)...);
}
// sugar:
auto operator->*( decltype(Op) const& ) const {
return [pf=pf, pv=pv](Args...args)->R {
return pf(pv, std::forward<Args>(args)...);
};
}
private:
void const* pv = nullptr;
R(*pf)(void const*, Args&&...) = nullptr;
};
auto do_show = [](auto&& x)->void {
x.show();
};
using showable_ref = opable_ref<do_show>;
A showable_ref
is a reference to any object that can be shown.
template<class T>
using il = std::initializer_list<T>;
for (auto &obj : il<showable_ref>{w3, w4, w5, t1})
(obj->*do_show)();
or
for (auto &obj : il<showable_ref>{w3, w4, w5, t1})
obj();
Live example.
opable_ref<lambda, Sig>
creates an object that represents a reference to something you could apply the lambda to, providing extra arguments as described in Sig
and the return value described in Sig
(defaulting to no extra arguments and returning void
).
At the point of construction, it remembers how to invoke Op
on the object, assuming you have a void pointer pointing at it, and stores a void pointer to the object.
A more robust, library-worthy version might split off that type erasure from the void pointer; we convert our lambda into something that can remember a type erased argument.
This would allow us to have a collection of type erased operations all acting on the same type erased object, like:
template<class T, auto Op, class R, class...Args>
concept opable = requires(T& t, Args&&...args) {
{ Op(t, std::forward<Args>(args)...) }->std::convertible_to<R>;
};
template<class T>struct tag_t{using type=T;};
template<class T>constexpr tag_t<T> tag={};
template<auto Op>
struct op_t{
template<class...Args>
decltype(auto) operator()(Args&&...args)const {
return op(std::forward<Args>(args)...);
}
};
template<auto Op>constexpr op_t<Op> op={};
template<auto Op, class Sig=void()>
struct eraseable;
template<auto Op, class R, class...Args>
struct eraseable<Op, R(Args...)> {
template<opable<Op, R, Args...> T>
eraseable( tag_t<T> ):
pf([](void const*pv, Args&&...args)->R{
return Op( *static_cast<T*>(const_cast<void*>(pv)), std::forward<Args>(args)... );
})
{}
auto operator->*( op_t<Op> op )const {
return [pf=pf](void const* pv, Args...args)->R {
pf(pv, std::forward<Args>(args)...);
};
}
R operator()(void const* pv, Args...args)const {
return ((*this)->*op<Op>)(pv, std::forward<Args>(args)...);
}
private:
R(*pf)(void const*, Args&&...)=nullptr;
};
template<class...erasure>
struct opable_ref {
template<class T>
opable_ref(T& t):
pv(std::addressof(t)),
operations(tag<T>)
{}
template<auto Op>
decltype(auto) operator->*( op_t<Op> )const {
return [pv=pv, ops=operations]<class...Args>(Args&&...args)->decltype(auto) {
return (ops->*op<Op>)(pv, std::forward<Args>(args)...);
};
}
private:
struct operations_t:
public erasure...
{
template<class T>
operations_t(tag_t<T> tag):
erasure(tag)...
{}
using erasure::operator->*...;
};
void const* pv = nullptr;
operations_t operations;
};
auto do_show = [](auto&x){x.show();};
auto do_hide = [](auto&x){x.hide();};
constexpr auto show = op<do_show>;
constexpr auto hide = op<do_hide>;
using erase_show = eraseable<do_show>;
using erase_hide = eraseable<do_hide>;
using showable_ref = opable_ref<erase_show, erase_hide>;
and now we can erase both show and hide, with point-of-use syntax looking like:
for (auto &obj : il<showable_ref>{w3, w4, w5, t1})
{
(obj->*show)();
(obj->*hide)();
}
Live example.
The opable_ref
takes up the space of 1 pointer, plus 1 (raw C function) pointer per operation. We could move the raw C function pointers into a table, where they only exist per type erased into opable_ref
- a vtable - with a static local trick.
template<class...erasure>
struct opable_ref {
template<class T>
opable_ref(T& t):
pv(std::addressof(t)),
operations(get_vtable(tag<T>))
{}
template<auto Op>
decltype(auto) operator->*( op_t<Op> )const {
return [pv=pv, ops=operations]<class...Args>(Args&&...args)->decltype(auto) {
return ((*ops)->*op<Op>)(pv, std::forward<Args>(args)...);
};
}
private:
struct operations_t:
public erasure...
{
template<class T>
operations_t(tag_t<T> tag):
erasure(tag)...
{}
using erasure::operator->*...;
};
void const* pv = nullptr;
operations_t const* operations = nullptr;
template<class T>
static operations_t const* get_vtable(tag_t<T> tag) {
static operations_t ops(tag);
return &ops;
}
};
overhead per opable_ref
is now 2 pointers, plus a table of one function pointer per type and per operation erased.
tuple
– Affectionatevirtual void show()
, it should also work with.size()
and std-containers. – Humfrey