Tener un objeto raíz limita lo que puedes hacer y lo que puede hacer el compilador, sin mucho beneficio.
Una clase raíz común hace posible crear contenedores de cualquier cosa y extraer lo que son con un dynamic_cast
, pero si necesitas contenedores de cualquier cosa, entonces algo similar a boost::any
puede hacerlo sin una clase raíz común. Y boost::any
también admite primitivas, incluso puede admitir la optimización de búfer pequeño y dejarlos casi "sin caja" en el lenguaje de Java.
C ++ admite y prospera en tipos de valor. Ambos literales, y programadores escriben tipos de valor. Los contenedores de C ++ almacenan, clasifican, codifican, consumen y producen tipos de valor de manera eficiente.
La herencia, especialmente el tipo de herencia monolítica que implican las clases base de Java, requiere tipos de "puntero" o "referencia" basados en tienda libre. Su identificador / puntero / referencia a los datos tiene un puntero a la interfaz de la clase, y polimorfamente podría representar otra cosa.
Aunque esto es útil en algunas situaciones, una vez que se haya casado con el patrón con una "clase base común", ha bloqueado toda su base de código en el costo y el equipaje de este patrón, incluso cuando no es útil .
Casi siempre sabe más sobre un tipo que "es un objeto", ya sea en el sitio de la llamada o en el código que lo usa.
Si la función es simple, escribir la función como una plantilla le proporciona un polimorfismo basado en el tiempo de compilación de tipo pato donde la información en el sitio de la llamada no se desecha. Si la función es más compleja, se puede realizar un borrado de tipo mediante el cual las operaciones uniformes en el tipo que desea realizar (por ejemplo, serialización y deserialización) se pueden crear y almacenar (en tiempo de compilación) para que sean consumidas (en tiempo de ejecución) por el Código en una unidad de traducción diferente.
Supongamos que tiene alguna biblioteca donde desea que todo sea serializable. Un enfoque es tener una clase base:
struct serialization_friendly {
virtual void write_to( my_buffer* ) const = 0;
virtual void read_from( my_buffer const* ) = 0;
virtual ~serialization_friendly() {}
};
Ahora, cada bit de código que escribes puede ser serialization_friendly
.
void serialize( my_buffer* b, serialization_friendly const* x ) {
if (x) x->write_to(b);
}
Excepto no un std::vector
, por lo que ahora necesita escribir cada contenedor. Y no esos enteros que obtuviste de esa gran biblioteca. Y no es el tipo que escribiste que no creías que fuera necesario serializar. Y no un tuple
, o un int
o un double
, o un std::ptrdiff_t
.
Tomamos otro enfoque:
void write_to( my_buffer* b, int x ) {
b->write_integer(x);
}
template<class T,
class=std::enable_if_t< void_t<
std::declval<T const*>()->write_to( std::declval<my_buffer*>()
> >
>
void write_to( my_buffer* b, T const* x ) {
if (x) x->write_to(b);
}
template<class T>
void serialize( my_buffer* b, T const& t ) {
write_to( b, t );
}
que consiste en, bueno, no hacer nada, aparentemente. Excepto que ahora podemos extender write_to
anulando write_to
como una función gratuita en el espacio de nombres de un tipo o un método en el tipo.
Incluso podemos escribir un poco de código de borrado de tipo:
namespace details {
struct can_serialize_pimpl {
virtual void write_to( my_buffer* ) const = 0;
virtual void read_from( my_buffer const* ) = 0;
virtual ~can_serialize_pimpl() {}
};
}
struct can_serialize {
void write_to( my_buffer* b ) const { pImpl->write_to(b); }
void read_from( my_buffer const* b ) { pImpl->read_from(b); }
std::unique_ptr<details::can_serialize_pimpl> pImpl;
template<class T> can_serialize(T&&);
};
namespace details {
template<class T>
struct can_serialize : can_serialize_pimpl {
std::decay_t<T>* t;
void write_to( my_buffer*b ) const final override {
serialize( b, std::forward<T>(*t) );
}
void read_from( my_buffer const* ) final override {
deserialize( b, std::forward<T>(*t) );
}
can_serialize(T&& in):t(&in) {}
};
}
template<class T> can_serialize::can_serialize<T>(T&&t):pImpl(
std::make_unique<details::can_serialize<T>>( std::forward<T>(t) );
) {}
y ahora podemos tomar un tipo arbitrario y colocarlo automáticamente en una interfaz can_serialize
que te permite invocar serialize
en un punto posterior a través de una interfaz virtual.
Entonces:
void writer_thingy( can_serialize s );
es una función que toma cualquier cosa que pueda serializarse, en lugar de
void writer_thingy( serialization_friendly const* s );
y el primero, a diferencia del segundo, puede manejar int
, std::vector<std::vector<Bob>>
automáticamente.
No tomó mucho escribirlo, especialmente porque este tipo de cosas es algo que rara vez quieres hacer, pero obtuvimos la capacidad de tratar cualquier cosa como serializable sin requerir un tipo de base.
Más aún, ahora podemos hacer que std::vector<T>
sea serializable como ciudadano de primera clase simplemente anulando write_to( my_buffer*, std::vector<T> const& )
. Con esa sobrecarga, se puede pasar a un can_serialize
y la serialización de std::vector
obtiene almacenado en un vtable y accedido por .write_to
.
En resumen, C ++ es lo suficientemente poderoso como para que pueda implementar las ventajas de una sola clase base sobre la marcha cuando sea necesario, sin tener que pagar el precio de una jerarquía de herencia forzada cuando no es necesario. Y los momentos en que se requiere la base única (falsificada o no) son razonablemente raros.
Cuando los tipos son realmente su identidad, y usted sabe lo que son, las oportunidades de optimización abundan. Los datos se almacenan localmente y de forma contigua (lo cual es muy importante para la facilidad de uso de la memoria caché en los procesadores modernos), los compiladores pueden entender fácilmente lo que hace una operación dada (en lugar de tener un puntero de método virtual opaco, tiene que saltar, dando lugar a un código desconocido en el otro lado) que permite que las instrucciones se reordenen de manera óptima, y que se peguen menos clavijas redondas en los agujeros redondos.