webingles videos tag questions pacho lesson leccion ingles estructura c++ type-erasure c++17 stdany

c++ - videos - Borrar tipo borrar, ¿alguna pregunta?



webingles leccion 95 (2)

Aquí está mi solución. Parece más corto que el de Yakk, y no usa std::aligned_storage y ubicación nueva. Además, admite los functores con estado y locales (lo que implica que tal vez nunca sea posible escribir super_any<&print> , ya que print podría ser una variable local).

any_method:

template<class F, class Sig> struct any_method; template<class F, class Ret, class... Args> struct any_method<F,Ret(Args...)> { F f; template<class T> static Ret invoker(any_method& self, boost::any& data, Args... args) { return self.f(boost::any_cast<T&>(data), std::forward<Args>(args)...); } using invoker_type = Ret (any_method&, boost::any&, Args...); };

make_any_method:

template<class Sig, class F> any_method<std::decay_t<F>,Sig> make_any_method(F&& f) { return { std::forward<F>(f) }; }

super_any:

template<class...OperationsToTypeErase> struct super_any { boost::any data; std::tuple<typename OperationsToTypeErase::invoker_type*...> operations = {}; template<class T, class ContainedType = std::decay_t<T>> super_any(T&& t) : data(std::forward<T>(t)) , operations((OperationsToTypeErase::template invoker<ContainedType>)...) {} template<class T, class ContainedType = std::decay_t<T>> super_any& operator=(T&& t) { data = std::forward<T>(t); operations = { (OperationsToTypeErase::template invoker<ContainedType>)... }; return *this; } };

operador -> *:

template<class...Ops, class F, class Sig, // SFINAE filter that an op matches: std::enable_if_t< std::disjunction< std::is_same<Ops, any_method<F,Sig>>... >{}, int> = 0 > auto operator->*( super_any<Ops...>& a, any_method<F,Sig> f) { auto fptr = std::get<typename any_method<F,Sig>::invoker_type*>(a.operations); return [fptr,f, &a](auto&&... args) mutable { return fptr(f, a.data, std::forward<decltype(args)>(args)...); }; }

Uso:

#include <iostream> auto print = make_any_method<void(std::ostream&)>( [](auto&& self, auto&& os){ os << self; } ); using printable_any = super_any<decltype(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 }

Live

Entonces, supongamos que quiero escribir borrado usando el borrado de texto.

Puedo crear pseudo-métodos para variantes que permiten un 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

Mi pregunta es, ¿cómo extiendo esto a un std::any ?

No se puede hacer "en bruto". Pero en el punto donde asignamos / construimos un std::any tenemos la información de tipo que necesitamos.

Entonces, en teoría, un any aumentado:

template<class...OperationsToTypeErase> struct super_any { std::any data; // or some transformation of OperationsToTypeErase? std::tuple<OperationsToTypeErase...> operations; // ?? what for ctor/assign/etc? };

de alguna manera podría volver a enlazar automáticamente algún código de modo que el tipo de sintaxis anterior funcionaría.

Idealmente, sería tan breve como el caso de la variante.

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); }

Ahora, ¿puedo mantener esto en un tipo , pero usar razonablemente la sintaxis lambda para simplificar las cosas?

Idealmente quiero:

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 }

o sintaxis similar. ¿Es esto imposible? Inviable? ¿Fácil?


Esta es una solución que usa C ++ 14 y boost::any , ya que no tengo un compilador de C ++ 17.

La sintaxis con la que terminamos es:

const auto print = make_any_method<void(std::ostream&)>([](auto&& p, std::ostream& t){ t << p << "/n"; }); super_any<decltype(print)> a = 7; (a->*print)(std::cout);

lo cual es casi óptimo Con lo que creo que son simples cambios en C ++ 17, debería verse así:

constexpr any_method<void(std::ostream&)> print = [](auto&& p, std::ostream& t){ t << p << "/n"; }; super_any<&print> a = 7; (a->*print)(std::cout);

En C ++ 17, mejoraría esto tomando un auto*... de punteros a any_method lugar del ruido de decltype .

Heredar públicamente de any es un poco arriesgado, ya que si alguien quita el any de la parte superior y lo modifica, la tuple de any_method_data estará desactualizada. Probablemente deberíamos imitar toda la interfaz en lugar de heredarla públicamente.

@dyp escribió una prueba de concepto en comentarios al OP. Esto se basa en su trabajo, limpiado con semántica de valor (robado de boost::any ) agregado. La solución basada en el puntero de @ cpplearner se usó para acortarla (¡gracias!), y luego agregué la optimización vtable además de eso.

Primero usamos una etiqueta para pasar los tipos:

template<class T>struct tag_t{constexpr tag_t(){};}; template<class T>constexpr tag_t<T> tag{};

Esta clase de rasgo obtiene la firma almacenada con un any_method :

Esto crea un tipo de puntero de función y una fábrica para dichos punteros de función, dado un 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)... ); }; } };

Ahora no queremos almacenar un puntero de función por operación en nuestro super_any . Entonces agrupamos los punteros de función en una tabla 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 ); } };

podríamos especializar esto para casos en los que vtable es pequeño (por ejemplo, 1 elemento) y usar punteros directos almacenados en clase en esos casos para mayor eficiencia.

Ahora comenzamos el super_any . Uso super_any_t para hacer la declaración de super_any un poco más fácil.

template<class...methods> struct super_any_t;

Esto busca los métodos que el super any admite para 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>{}> {};

Este es el puntero de pseudo-método, como print , que creamos globalmente y const .

Almacenamos el objeto con el que construimos esto dentro del any_method . Tenga en cuenta que si lo construye con un objeto que no sea lambda, las cosas pueden ponerse difíciles, ya que el tipo de este any_method usa como parte del mecanismo de envío.

template<class Sig, class F> struct any_method { using signature=Sig; private: F f; public: template<class Any, // SFINAE testing that one of the Anys''s matches this 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 ) { // we don''t use the value of the any_method, because each any_method has // a unique type (!) and we check that one of the auto*''s in the super_any // already has a pointer to us. We then dispatch to the 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)...); } };

Un método de fábrica, no necesario en C ++ 17, creo:

template<class Sig, class F> any_method<Sig, std::decay_t<F>> make_any_method( F&& f ) { return {std::forward<F>(f)}; }

Este es el aumentado any . Es a la vez un any , y lleva consigo un conjunto de punteros de función de borrado de tipo que cambian cada vez que el contenido contiene:

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

Debido a que almacenamos any_method s como objetos const , esto hace que super_any un poco más fácil:

template<class...Ts> using super_any = super_any_t< std::remove_const_t<std::remove_reference_t<Ts>>... >;

Código de prueba:

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)> a = 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; a = d; (a->*print)(std::cout); (a->*wprint)(std::wcout); (a2->*print)(std::cout); (a2->*wprint)(std::wcout); // a = X{}; // generates an error if you try to store a non-printable }

ejemplo en vivo

El mensaje de error cuando intento almacenar una struct X{}; no imprimible struct X{}; dentro del super_any parece razonable al menos en el super_any :

main.cpp:150:87: error: invalid operands to binary expression (''std::ostream'' (aka ''basic_ostream<char>'') and ''X'') const auto x0 = make_any_method<void(std::ostream&)>([](auto&& p, std::ostream& t){ t << p << "/n"; });

Esto sucede en el momento en que intenta asignar la X{} a super_any<decltype(x0)> .

La estructura de any_method es suficientemente compatible con el pseudo_method que actúa de manera similar en las variantes que probablemente puedan fusionarse.

Utilicé una vtable manual aquí para mantener la sobrecarga de borrado de tipo en 1 puntero por super_any . Esto agrega un costo de redireccionamiento a cada llamada a any_method. Podríamos almacenar los punteros directamente en super_any muy fácilmente, y no sería difícil hacer que ese parámetro sea super_any . En cualquier caso, en el caso del método 1 borrado, deberíamos almacenarlo directamente.

Dos any_method diferentes del mismo tipo (digamos, ambos que contienen un puntero de función) generan el mismo tipo de super_any . Esto causa problemas en la búsqueda.

Distinguir entre ellos es un poco complicado. Si cambiamos el super_any para tomar auto* any_method , podríamos agrupar todos los any_method tipo idéntico any_method en la tupla vtable, luego hacer una búsqueda lineal de un puntero coincidente si hay más de 1. La búsqueda lineal debe optimizarse lejos por el compilador a menos que esté haciendo algo loco como pasar una referencia o un puntero al any_method particular que estamos usando.

Sin embargo, eso parece estar fuera del alcance de esta respuesta; La existencia de esa mejora es suficiente por ahora.

Además, se puede agregar un ->* que toma un puntero (¡o incluso una referencia!) En el lado izquierdo, permitiéndole detectar esto y pasarlo también a la lambda. Esto puede hacer que sea realmente un "método cualquiera", ya que funciona en variantes, super_anys y punteros con ese método.

Con un poco de if constexpr funciona, la lambda puede ramificarse al hacer una llamada ADL o de método en todos los casos.

Esto debería darnos:

(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);

con any_method simplemente "haciendo lo correcto" (que está alimentando el valor a std::cout << ).