c++ - Type erasing type erasure, `any` questions? -


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 , constly.

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_methods 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 } 

live example.

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_methods 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_methods 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