Ter um objeto raiz limita o que você pode fazer e o que o compilador pode fazer, sem muito retorno.
Uma classe raiz comum torna possível criar contêineres de qualquer coisa e extrair o que eles são com a dynamic_cast
, mas se você precisar de contêineres de qualquer coisa, algo semelhante boost::any
pode ser feito sem uma classe raiz comum. E boost::any
também suporta primitivos - ele pode até suportar a otimização de pequenos buffers e deixá-los quase "sem caixa" na linguagem Java.
O C ++ suporta e prospera em tipos de valor. Literais e tipos de valores escritos pelo programador. Os contêineres C ++ armazenam, classificam, hash, consomem e produzem tipos de valor com eficiência.
A herança, especialmente o tipo de herança base do estilo Java de herança monolítica, requer tipos de "ponteiro" ou "referência" baseados em armazenamento livre. Seu identificador / ponteiro / referência aos dados contém um ponteiro para a interface da classe e, polimorficamente, pode representar outra coisa.
Embora isso seja útil em algumas situações, depois de se casar com o padrão com uma "classe base comum", você bloqueia toda a sua base de códigos no custo e na bagagem desse padrão, mesmo quando não é útil.
Quase sempre, você sabe mais sobre um tipo do que "ele é um objeto" no site de chamada ou no código que o utiliza.
Se a função for simples, escrever a função como modelo fornece um polimorfismo baseado em tempo de compilação do tipo pato, onde as informações no site de chamada não são descartadas. Se a função for mais complexa, o apagamento do tipo pode ser feito pelo qual as operações uniformes do tipo que você deseja executar (por exemplo, serialização e desserialização) podem ser construídas e armazenadas (em tempo de compilação) para serem consumidas (em tempo de execução) pelo código em uma unidade de tradução diferente.
Suponha que você tenha alguma biblioteca na qual deseja que tudo seja serializado. Uma abordagem é ter uma classe base:
struct serialization_friendly {
virtual void write_to( my_buffer* ) const = 0;
virtual void read_from( my_buffer const* ) = 0;
virtual ~serialization_friendly() {}
};
Agora todo código que você escreve pode ser serialization_friendly
.
void serialize( my_buffer* b, serialization_friendly const* x ) {
if (x) x->write_to(b);
}
Exceto não um std::vector
, agora você precisa escrever todos os contêineres. E não os números inteiros que você obteve dessa biblioteca bignum. E não o tipo que você escreveu que achava que não precisava ser serializado. E não um tuple
, ou um int
ou um double
, ou um std::ptrdiff_t
.
Adotamos outra abordagem:
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 em, aparentemente, não fazer nada. Exceto agora, podemos estender write_to
substituindo write_to
como uma função livre no espaço para nome de um tipo ou método no tipo.
Podemos até escrever um pouco de código de apagamento 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) );
) {}
e agora podemos pegar um tipo arbitrário e encaixotá-lo automaticamente em uma can_serialize
interface que permite invocar serialize
posteriormente através de uma interface virtual.
Assim:
void writer_thingy( can_serialize s );
é uma função que aceita tudo o que pode ser serializado, em vez de
void writer_thingy( serialization_friendly const* s );
eo primeiro, ao contrário do segundo, ele pode manipular int
, std::vector<std::vector<Bob>>
automaticamente.
Não demorou muito para escrevê-lo, especialmente porque esse tipo de coisa é algo que você raramente quer fazer, mas adquirimos a capacidade de tratar qualquer coisa como serializável sem exigir um tipo de base.
Além disso, agora podemos tornar o std::vector<T>
serializável como cidadão de primeira classe simplesmente substituindo write_to( my_buffer*, std::vector<T> const& )
- com essa sobrecarga, ele pode ser passado para um can_serialize
e a serialização das informações std::vector
é armazenada em uma tabela e acessada por .write_to
.
Em suma, o C ++ é poderoso o suficiente para que você possa implementar as vantagens de uma única classe base rapidamente quando necessário, sem ter que pagar o preço de uma hierarquia de herança forçada quando não for necessário. E os horários em que a base única (falsificada ou não) é necessária são razoavelmente raros.
Quando os tipos são na verdade sua identidade e você sabe o que são, as oportunidades de otimização são muitas. Os dados são armazenados localmente e de forma contígua (o que é altamente importante para a facilidade de cache nos processadores modernos), os compiladores podem entender facilmente o que uma determinada operação faz (em vez de ter um ponteiro de método virtual opaco que ele deve pular, levando a códigos desconhecidos no outro lado), que permite que as instruções sejam reordenadas de maneira ideal, e menos pinos redondos são martelados em orifícios redondos.