so, suppose want type erase using type erasure.
i can create pseudo-methods variants enable natural:
pseudo_method print = [](auto&& self, auto&& os){ os << self; }; std::variant<a,b,c> var = // create variant of type b or c (var->*print)(std::cout); // print out without knowing
my question is, how extend std::any
?
it cannot done "in raw". @ point assign to/construct std::any
have type information need.
so, in theory, augmented any
:
template<class...operationstotypeerase> struct super_any { std::any data; // or transformation of operationstotypeerase? std::tuple<operationstotypeerase...> operations; // ?? ctor/assign/etc? };
could somehow automatically rebind code such above type of syntax work.
ideally terse in use variant case is.
template<class...ops, class op, // sfinae filter 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 keep type, yet reasonably use lambda syntax keep things simple?
ideally 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 printing data attached int main() { (bob->*print)(std::cout); // prints 7 bob = 3.14159; (bob->*print)(std::cout); // prints 3.14159 }
or similar syntax. impossible? infeasible? easy?
this solution uses c++14 , boost::any
, don't have c++17 compiler.
the syntax end is:
const auto print = make_any_method<void(std::ostream&)>([](auto&& p, std::ostream& t){ t << p << "\n"; }); super_any<decltype(print)> = 7; (a->*print)(std::cout);
which optimal. believe simple c++17 changes, should like:
constexpr any_method<void(std::ostream&)> print = [](auto&& p, std::ostream& t){ t << p << "\n"; }; super_any<&print> = 7; (a->*print)(std::cout);
in c++17 i'd improve taking auto*...
of pointers any_method
instead of decltype
noise.
inheriting publicly any
bit risky, if takes any
off top , modifies it, tuple
of any_method_data
out of date. should mimic entire any
interface rather inherit publicly.
@dyp wrote proof of concept in comments op. based off work, cleaned value-semantics (stolen boost::any
) added. @cpplearner's pointer-based solution used shorten (thanks!), , added vtable optimization on top of that.
first use tag pass around types:
template<class t>struct tag_t{constexpr tag_t(){};}; template<class t>constexpr tag_t<t> tag{};
this trait class gets signature stored any_method
:
this creates function pointer type, , factory said function pointers, given any_method
:
template<class any_method, class sig=any_sig_from_method<any_method>> struct any_method_function; template<class any_method, class r, class...args> struct any_method_function<any_method, r(args...)> { using type = r(*)(boost::any&, any_method const*, args...); template<class t> type operator()( tag_t<t> )const{ return [](boost::any& self, any_method const* method, args...args) { return (*method)( boost::any_cast<t&>(self), decltype(args)(args)... ); }; } };
now don't want store function pointer per operation in our super_any
. bundle function pointers vtable:
template<class...any_methods> using any_method_tuple = std::tuple< typename any_method_function<any_methods>::type... >; template<class...any_methods, class t> any_method_tuple<any_methods...> make_vtable( tag_t<t> ) { return std::make_tuple( any_method_function<any_methods>{}(tag<t>)... ); } template<class...methods> struct any_methods { private: any_method_tuple<methods...> const* vtable = 0; template<class t> static any_method_tuple<methods...> const* get_vtable( tag_t<t> ) { static const auto table = make_vtable<methods...>(tag<t>); return &table; } public: any_methods() = default; template<class t> any_methods( tag_t<t> ): vtable(get_vtable(tag<t>)) {} any_methods& operator=(any_methods const&)=default; template<class t> void change_type( tag_t<t> ={} ) { vtable = get_vtable(tag<t>); } template<class any_method> auto get_invoker( tag_t<any_method> ={} ) const { return std::get<typename any_method_function<any_method>::type>( *vtable ); } };
we specialize cases vtable small (for example, 1 item), , use direct pointers stored in-class in cases efficiency.
now start super_any
. use super_any_t
make declaration of super_any
bit easier.
template<class...methods> struct super_any_t;
this searches methods super supports sfinae:
template<class super_any, class method> struct super_method_applies : std::false_type {}; template<class m0, class...methods, class method> struct super_method_applies<super_any_t<m0, methods...>, method> : std::integral_constant<bool, std::is_same<m0, method>{} || super_method_applies<super_any_t<methods...>, method>{}> {};
this pseudo-method pointer, print
, create globally , const
ly.
we store object construct inside any_method
. note if construct non-lambda things can hairy, type of any_method
used part of dispatch mechanism.
template<class sig, class f> struct any_method { using signature=sig; private: f f; public: template<class any, // sfinae testing 1 of anys's matches type: std::enable_if_t< super_method_applies< std::decay_t<any>, any_method >{}, int>* =nullptr > friend auto operator->*( any&& self, any_method const& m ) { // don't use value of any_method, because each any_method has // unique type (!) , check 1 of auto*'s in super_any // has pointer us. dispatch corresponding // any_method_data... return [&self, invoke = self.get_invoker(tag<any_method>), m](auto&&...args)->decltype(auto) { return invoke( decltype(self)(self), &m, decltype(args)(args)... ); }; } any_method( f fin ):f(std::move(fin)) {} template<class...args> decltype(auto) operator()(args&&...args)const { return f(std::forward<args>(args)...); } };
a factory method, not needed in c++17 believe:
template<class sig, class f> any_method<sig, std::decay_t<f>> make_any_method( f&& f ) { return {std::forward<f>(f)}; }
this augmented any
. both any
, , carries around bundle of type-erasure function pointers change whenever contained any
does:
template<class... methods> struct super_any_t:boost::any, any_methods<methods...> { private: template<class t> t* get() { return boost::any_cast<t*>(this); } public: template<class t, std::enable_if_t< !std::is_same<std::decay_t<t>, super_any_t>{}, int>* =nullptr > super_any_t( t&& t ): boost::any( std::forward<t>(t) ) { using dt=std::decay_t<t>; this->change_type( tag<dt> ); } super_any_t()=default; super_any_t(super_any_t&&)=default; super_any_t(super_any_t const&)=default; super_any_t& operator=(super_any_t&&)=default; super_any_t& operator=(super_any_t const&)=default; template<class t, std::enable_if_t< !std::is_same<std::decay_t<t>, super_any_t>{}, int>* =nullptr > super_any_t& operator=( t&& t ) { ((boost::any&)*this) = std::forward<t>(t); using dt=std::decay_t<t>; this->change_type( tag<dt> ); return *this; } };
because store any_method
s const
objects, makes making super_any
bit easier:
template<class...ts> using super_any = super_any_t< std::remove_const_t<std::remove_reference_t<ts>>... >;
test code:
const auto print = make_any_method<void(std::ostream&)>([](auto&& p, std::ostream& t){ t << p << "\n"; }); const auto wprint = make_any_method<void(std::wostream&)>([](auto&& p, std::wostream& os ){ os << p << l"\n"; }); const auto wont_work = make_any_method<void(std::ostream&)>([](auto&& p, std::ostream& t){ t << p << "\n"; }); struct x {}; int main() { super_any<decltype(print), decltype(wprint)> = 7; super_any<decltype(print), decltype(wprint)> a2 = 7; (a->*print)(std::cout); (a->*wprint)(std::wcout); // (a->*wont_work)(std::cout); double d = 4.2; = d; (a->*print)(std::cout); (a->*wprint)(std::wcout); (a2->*print)(std::cout); (a2->*wprint)(std::wcout); // = x{}; // generates error if try store non-printable }
the error message when try store non-printable struct x{};
inside super_any
seems reasonable @ least on clang:
main.cpp:150:87: error: invalid operands binary expression ('std::ostream' (aka 'basic_ostream<char>') , 'x') const auto x0 = make_any_method<void(std::ostream&)>([](auto&& p, std::ostream& t){ t << p << "\n"; });
this happens moment try assign x{}
super_any<decltype(x0)>
.
the structure of any_method
sufficiently compatible pseudo_method
acts on variants can merged.
i used manual vtable here keep type erasure overhead 1 pointer per super_any
. adds redirection cost every any_method call. store pointers directly in super_any
easily, , wouldn't hard make parameter super_any
. in case, in 1 erased method case, should store directly.
two different any_method
s of same type (say, both containing function pointer) spawn same kind of super_any
. causes problems @ lookup.
distinguishing between them bit tricky. if changed super_any
take auto* any_method
, bundle of identical-type any_method
s in vtable tuple, linear search matching pointer if there more 1. linear search should optimized away compiler unless doing crazy passing reference or pointer particular any_method
using.
that seems beyond scope of answer, however; existence of improvement enough now.
in addition, ->*
takes pointer (or reference!) on left hand side can added, letting detect , pass lambda well. can make "any method" in works on variants, super_anys, , pointers method.
with bit of if constexpr
work, lambda can branch on doing adl or method call in every case.
this should give us:
(7->*print)(std::cout); ((super_any<&print>)(7)->*print)(std::cout); // c++17 version of above syntax ((std::variant<int, double>{7})->*print)(std::cout); int* ptr = new int(7); (ptr->*print)(std::cout); (std::make_unique<int>(7)->*print)(std::cout); (std::make_shared<int>(7)->*print)(std::cout);
with any_method
"doing right thing" (which feeding value std::cout <<
).
Comments
Post a Comment