Avoir un seul objet racine limite ce que vous pouvez faire et ce que le compilateur peut faire, sans grand profit.
Une classe racine commune permet de créer des conteneurs-de-n'importe quoi et d'extraire ce qu'ils sont avec un dynamic_cast
, mais si vous avez besoin de conteneurs-de-n'importe quoi alors quelque chose de semblable boost::any
peut le faire sans une classe racine commune. Et boost::any
prend également en charge les primitives - il peut même prendre en charge l’optimisation de la mémoire tampon réduite et les laisser presque "décompressées" dans le jargon Java.
C ++ prend en charge et prospère sur les types de valeur. Les deux littéraux et les types de valeur écrits par le programmeur. Les conteneurs C ++ stockent, trient, hachent, consomment et produisent efficacement des types de valeur.
L'héritage, en particulier le type d'héritage monolithique qu'impliquent les classes de base de style Java, nécessite des types "pointeur" ou "référence" basés sur des magasins gratuits. Votre handle / pointeur / référence à data contient un pointeur sur l'interface de la classe, et polymorphically pourrait représenter autre chose.
Bien que cela soit utile dans certaines situations, une fois que vous vous êtes marié au modèle avec une "classe de base commune", vous avez verrouillé l'intégralité de votre base de code sur le coût et le bagage de ce modèle, même lorsque cela n'était pas utile.
Presque toujours, vous en savez plus sur un type que "c'est un objet" sur le site appelant ou dans le code qui l'utilise.
Si la fonction est simple, l'écrire en tant que modèle vous donne un polymorphisme basé sur le temps de compilation du type de canard où les informations sur le site appelant ne sont pas rejetées. Si la fonction est plus complexe, l’effacement des types peut être effectué de sorte que les opérations uniformes sur le type que vous voulez effectuer (par exemple, la sérialisation et la désérialisation) puissent être générées et stockées (au moment de la compilation) pour être consommées (au moment de l’exécution) par le code dans une unité de traduction différente.
Supposons que vous ayez une bibliothèque où vous voulez que tout soit sérialisable. Une approche consiste à avoir une classe de base:
struct serialization_friendly {
virtual void write_to( my_buffer* ) const = 0;
virtual void read_from( my_buffer const* ) = 0;
virtual ~serialization_friendly() {}
};
Maintenant, chaque morceau de code que vous écrivez peut être serialization_friendly
.
void serialize( my_buffer* b, serialization_friendly const* x ) {
if (x) x->write_to(b);
}
Sauf que pas un std::vector
, alors maintenant vous devez écrire chaque conteneur. Et pas les entiers que vous avez obtenus de cette bibliothèque bignum. Et pas ce type, vous avez écrit que vous ne pensiez pas besoin de sérialisation. Et pas un tuple
, ou un int
ou un double
, ou un std::ptrdiff_t
.
Nous prenons une autre approche:
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 );
}
qui consiste à, eh bien, ne rien faire, apparemment. Sauf que maintenant nous pouvons étendre write_to
en remplaçant en write_to
tant que fonction libre dans l'espace de nommage d'un type ou une méthode dans le type.
On peut même écrire un peu de code d'effacement de type:
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) );
) {}
et nous pouvons maintenant prendre un type arbitraire et le mettre automatiquement dans une can_serialize
interface qui vous permet d’invoquer serialize
ultérieurement via une interface virtuelle.
Alors:
void writer_thingy( can_serialize s );
est une fonction qui prend tout ce qui peut sérialiser, au lieu de
void writer_thingy( serialization_friendly const* s );
et le premier, contrairement au second, il peut gérer int
, std::vector<std::vector<Bob>>
automatiquement.
Cela n'a pas pris beaucoup de temps pour l'écrire, en particulier parce que ce genre de chose est quelque chose que vous ne voulez que rarement, mais nous avons gagné la possibilité de traiter n'importe quoi comme sérialisable sans nécessiter de type de base.
Quoi de plus, nous pouvons maintenant rendre std::vector<T>
sérialisable en tant que citoyen de premier ordre simplement en écrasant write_to( my_buffer*, std::vector<T> const& )
- avec cette surcharge, il peut être passé à un can_serialize
et la sérialisabilité des std::vector
objets est stockée dans une table virtuelle et accessible par .write_to
.
En bref, le C ++ est suffisamment puissant pour que vous puissiez implémenter les avantages d’une classe de base unique à la volée, sans avoir à payer le prix d’une hiérarchie à héritage forcé s’il n’est pas requis. Et les moments où la seule base (truquée ou non) est requise sont relativement rares.
Lorsque les types sont réellement leur identité et que vous savez ce qu’ils sont, les possibilités d’optimisation abondent. Les données sont stockées localement et de manière contiguë (ce qui est très important pour la convivialité du cache sur les processeurs modernes), les compilateurs peuvent facilement comprendre le fonctionnement d’une opération donnée de l’autre côté), ce qui permet de réorganiser les instructions de manière optimale, et moins de chevilles rondes sont enfoncées dans des trous ronds.