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