Ter um objeto raiz limita o que você pode fazer e o que o compilador pode fazer, sem muito retorno.
Uma classe raiz comum possibilita a criação de containers-de-qualquer coisa e extrai o que eles são com dynamic_cast
, mas se você precisar de containers-of-anything então algo como boost::any
pode fazer isso sem uma classe raiz comum. E boost::any
também suporta primitivos - pode até mesmo suportar a pequena otimização de buffer e deixá-los quase "sem caixa" na linguagem Java.
O C ++ suporta e prospera em tipos de valor. Ambos os literais e tipos de valor escritos do programador. Os contêineres C ++ armazenam, classificam, consolam, consomem e produzem tipos de valor com eficiência.
Herança, especialmente o tipo de herança de herança monolítica que as classes básicas de estilo Java implicam, requer tipos de "ponteiro" ou "referência" baseados em armazenamento livre. Seu identificador / ponteiro / referência a dados contém um ponteiro para a interface da classe e pode representar algo diferente polimorficamente.
Embora isso seja útil em algumas situações, uma vez que você tenha se casado com o padrão com uma "classe base comum", você terá bloqueado toda a sua base de códigos no custo e na bagagem desse padrão, mesmo quando isso não for útil. .
Quase sempre você sabe mais sobre um tipo do que "é um objeto" no site de chamada ou no código que o usa.
Se a função for simples, gravar a função como um modelo fornecerá um polimorfismo baseado no tempo de compilação do tipo duck, no qual as informações no site de chamada não serão descartadas. Se a função for mais complexa, o tipo de exclusão pode ser feito de modo que as operações uniformes no tipo que você deseja executar (digamos, serialização e desserialização) possam 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 onde queira que tudo seja serializável. 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, cada 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 aqueles inteiros que você obteve daquela biblioteca bignum. E não esse tipo que você escreveu que não achava necessário serialização. E não um tuple
, ou um int
ou um double
ou um std::ptrdiff_t
.
Nós 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, bem, não fazer nada, aparentemente. Exceto agora, podemos estender write_to
sobrescrevendo write_to
como uma função livre no namespace de um tipo ou método no tipo.
Podemos até escrever um pouco de código de eliminação de tipos:
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 usar um tipo arbitrário e encaixá-lo automaticamente em uma interface can_serialize
que permite invocar serialize
posteriormente usando uma interface virtual.
Então:
void writer_thingy( can_serialize s );
é uma função que pega qualquer coisa que possa serializar, em vez de
void writer_thingy( serialization_friendly const* s );
e o 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 ganhamos a habilidade de tratar qualquer coisa como serializável sem precisar de um tipo base.
Além disso, agora podemos tornar std::vector<T>
serializável como um 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 o serializabilty do std::vector
fica armazenado em uma tabela e acessado por .write_to
.
Em suma, o C ++ é poderoso o suficiente para que você possa implementar as vantagens de uma única classe base on-the-fly 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 momentos em que a base única (falsificada ou não) é necessária é razoavelmente rara.
Quando os tipos são realmente sua identidade e você sabe quais são, as oportunidades de otimização são abundantes. Os dados são armazenados localmente e contíguamente (o que é muito 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 para pular, levando a código desconhecido no outro lado) que permite que as instruções sejam otimamente reordenadas, e menos pinos redondos são martelados em buracos redondos.