So, suppose I want to type erase using type erasure.
I can create pseudo-methods for variants that enable a natural:
pseudo_method print = [](auto&& self, auto&& os){ os << self; };
std::variant<A,B,C> var = // create a variant of type A B or C
(var->*print)(std::cout); // print it out without knowing what it is
My question is, how do I extend this to a std::any
?
It cannot be done "in the raw". But at the point where we assign to/construct a std::any
we have the type information we need.
So, in theory, an augmented any
:
template<class...OperationsToTypeErase>
struct super_any {
std::any data;
// or some transformation of OperationsToTypeErase?
std::tuple<OperationsToTypeErase...> operations;
// ?? what for ctor/assign/etc?
};
could somehow automatically rebind some code such that the above type of syntax would work.
Ideally it would be as terse in use as the variant case is.
template<class...Ops, class Op,
// SFINAE filter that an op matches:
std::enable_if_t< std::disjunction< std::is_same<Ops, Op>... >{}, int>* =nullptr
>
decltype(auto) operator->*( super_any<Ops...>& a, any_method<Op> ) {
return std::get<Op>(a.operations)(a.data);
}
Now can I keep this to a type, yet reasonably use the lambda syntax to keep things simple?
Ideally I want:
any_method<void(std::ostream&)> print =
[](auto&& self, auto&& os){ os << self; };
using printable_any = make_super_any<&print>;
printable_any bob = 7; // sets up the printing data attached to the any
int main() {
(bob->*print)(std::cout); // prints 7
bob = 3.14159;
(bob->*print)(std::cout); // prints 3.14159
}
or similar syntax. Is this impossible? Infeasible? Easy?
Here's my solution. It looks shorter than Yakk's, and it does not use
std::aligned_storage
and placement new. It additionally supports stateful and local functors (which implies that it might never be possible to writesuper_any<&print>
, sinceprint
could be a local variable).any_method:
make_any_method:
super_any:
operator->*:
Usage:
Live
This is a solution that uses C++14 and
boost::any
, as I don't have a C++17 compiler.The syntax we end up with is:
which is almost optimal. With what I believe to be simple C++17 changes, it should look like:
In C++17 I'd improve this by taking a
auto*...
of pointers toany_method
instead of thedecltype
noise.Inheriting publicly from
any
is a bit risky, as if someone takes theany
off the top and modifies it, thetuple
ofany_method_data
will be out of date. Probably we should just mimic the entireany
interface rather than inherit publicly.@dyp wrote a proof of concept in comments to the OP. This is based off his work, cleaned up with value-semantics (stolen from
boost::any
) added. @cpplearner's pointer-based solution was used to shorten it (thanks!), and then I added the vtable optimization on top of that.First we use a tag to pass around types:
This trait class gets the signature stored with an
any_method
:This creates a function pointer type, and a factory for said function pointers, given an
any_method
:Now we don't want to store a function pointer per operation in our
super_any
. So we bundle up the function pointers into a vtable:we could specialize this for a cases where the vtable is small (for example, 1 item), and use direct pointers stored in-class in those cases for efficiency.
Now we start the
super_any
. I usesuper_any_t
to make the declaration ofsuper_any
a bit easier.This searches the methods that the super any supports for SFINAE:
This is the pseudo-method pointer, like
print
, that we create globally andconst
ly.We store the object we construct this with inside the
any_method
. Note that if you construct it with a non-lambda things can get hairy, as the type of thisany_method
is used as part of the dispatch mechanism.A factory method, not needed in C++17 I believe:
This is the augmented
any
. It is both anany
, and it carries around a bundle of type-erasure function pointers that change whenever the containedany
does:Because we store the
any_method
s asconst
objects, this makes making asuper_any
a bit easier:Test code:
live example.
The error message when I try to store a non-printable
struct X{};
inside thesuper_any
seems reasonable at least on clang:this happens the moment you try to assign the
X{}
into thesuper_any<decltype(x0)>
.The structure of the
any_method
is sufficiently compatible with thepseudo_method
that acts similarly on variants that they can probably be merged.I used a manual vtable here to keep the type erasure overhead to 1 pointer per
super_any
. This adds a redirection cost to every any_method call. We could store the pointers directly in thesuper_any
very easily, and it wouldn't be hard to make that a parameter tosuper_any
. In any case, in the 1 erased method case, we should just store it directly.Two different
any_method
s of the same type (say, both containing a function pointer) spawn the same kind ofsuper_any
. This causes problems at lookup.Distinguishing between them is a bit tricky. If we changed the
super_any
to takeauto* any_method
, we could bundle all of the identical-typeany_method
s up in the vtable tuple, then do a linear search for a matching pointer if there are more than 1. The linear search should be optimized away by the compiler unless you are doing something crazy like passing a reference or pointer to which particularany_method
we are using.That seems beyond the scope of this answer, however; the existence of that improvement is enough for now.
In addition, a
->*
that takes a pointer (or even reference!) on the left hand side can be added, letting it detect this and pass that to the lambda as well. This can make it truly an "any method" in that it works on variants, super_anys, and pointers with that method.With a bit of
if constexpr
work, the lambda can branch on doing an ADL or a method call in every case.This should give us:
with the
any_method
just "doing the right thing" (which is feeding the value tostd::cout <<
).