Je me demande quels sont les avantages possibles de la copie sur écriture? Naturellement, je ne m'attends pas à des opinions personnelles, mais à des scénarios pratiques du monde réel où cela peut être techniquement et pratiquement bénéfique de manière tangible. Et par tangible, je veux dire quelque chose de plus que vous sauver la saisie d'un &
caractère.
Pour clarifier, cette question se situe dans le contexte des types de données, où l'affectation ou la construction de copie crée une copie implicite superficielle, mais les modifications qui y sont apportées créent une copie profonde implicite et lui appliquent les modifications à la place de l'objet d'origine.
La raison pour laquelle je pose la question est que je ne trouve aucun avantage à avoir COW comme comportement implicite par défaut. J'utilise Qt, qui a implémenté COW pour beaucoup de types de données, pratiquement tous qui ont un stockage sous-jacent alloué dynamiquement. Mais en quoi cela profite-t-il vraiment à l'utilisateur?
Un exemple:
QString s("some text");
QString s1 = s; // now both s and s1 internally use the same resource
qDebug() << s1; // const operation, nothing changes
s1[o] = z; // s1 "detaches" from s, allocates new storage and modifies first character
// s is still "some text"
Que gagnons-nous en utilisant COW dans cet exemple?
Si tout ce que nous avons l'intention de faire, c'est d'utiliser des opérations const, s1
est redondant, autant l'utiliser s
.
Si nous avons l'intention de changer la valeur, alors COW ne retarde la copie de ressource que jusqu'à la première opération non-const, au prix (quoique minime) de l'incrémentation du nombre de références pour le partage implicite et le détachement du stockage partagé. Il semble que tous les frais généraux impliqués dans COW soient inutiles.
Ce n'est pas très différent dans le contexte de la transmission de paramètres - si vous n'avez pas l'intention de modifier la valeur, passez comme référence const, si vous voulez modifier, vous faites soit une copie profonde implicite si vous ne voulez pas modifier l'objet d'origine, ou passez par référence si vous souhaitez le modifier. Encore une fois, COW semble être une surcharge inutile qui ne permet rien, et ajoute seulement une limitation selon laquelle vous ne pouvez pas modifier la valeur d'origine même si vous le souhaitez, car tout changement se détachera de l'objet d'origine.
Donc, selon que vous connaissez COW ou que vous n'en avez pas conscience, cela peut entraîner un code avec une intention obscure et des frais généraux inutiles, ou un comportement complètement déroutant qui ne correspond pas aux attentes et vous laisse vous gratter la tête.
Il me semble qu'il existe des solutions plus efficaces et plus lisibles, que vous souhaitiez éviter une copie en profondeur inutile ou que vous ayez l'intention d'en créer une. Alors, où est l'avantage pratique de la vache? Je suppose qu'il doit y avoir un certain avantage car il est utilisé dans un cadre aussi populaire et puissant.
De plus, d'après ce que j'ai lu, COW est désormais explicitement interdit dans la bibliothèque standard C ++. Je ne sais pas si les con que j'y vois ont quelque chose à voir avec ça, mais de toute façon, il doit y avoir une raison à cela.
[]
opérateur. Donc, COW permet une mauvaise conception - cela ne ressemble pas à beaucoup d'avantages :) Le point dans le dernier paragraphe semble valide, mais je ne suis pas moi-même un grand fan du comportement implicite - les gens ont tendance à le prendre pour acquis, puis ont du mal à comprendre pourquoi le code ne fonctionne pas comme prévu, et continuez à vous demander jusqu'à ce qu'ils découvrent ce qui est caché derrière le comportement implicite.const_cast
semble qu'il puisse casser COW aussi facilement qu'il peut casser en passant par référence const. Par exemple,QString::constData()
renvoie unconst QChar *
-const_cast
cela et COW s'effondre - vous allez muter les données de l'objet d'origine.char*
évidemment pas au courant). Quant au comportement implicite, je pense que vous avez raison, il y a des problèmes avec cela. La conception de l'API est un équilibre constant entre les deux extrêmes. Trop implicite, et les gens commencent à se fier à un comportement spécial comme s'il faisait de facto partie de la spécification. Trop explicite et l'API devient trop compliquée car vous exposez trop de détails sous-jacents qui n'étaient pas vraiment importants et qui sont soudainement écrits dans vos spécifications d'API.string
classes ont un comportement COW parce que les concepteurs du compilateur ont remarqué qu'un grand corps de code copiait des chaînes plutôt que d'utiliser const-reference. S'ils ajoutaient COW, ils pourraient optimiser ce cas et rendre plus de gens heureux (et c'était légal, jusqu'à C ++ 11). J'apprécie leur position: alors que je passe toujours mes chaînes par référence const, j'ai vu toutes ces ordures syntaxiques qui nuisent à la lisibilité. Je déteste écrireconst std::shared_ptr<const std::string>&
juste pour capturer la bonne sémantique!Pour les chaînes et autres, il semble que cela pessimiserait des cas d'utilisation plus courants qu'improbable, car le cas commun pour les chaînes est souvent de petites chaînes, et là, les frais généraux de COW auraient tendance à dépasser de loin le coût de la simple copie de la petite chaîne. Une petite optimisation de la mémoire tampon me semble beaucoup plus logique pour éviter l'allocation de tas dans de tels cas au lieu des copies de chaînes.
Cependant, si vous avez un objet plus lourd, comme un androïde, et que vous vouliez le copier et simplement remplacer son bras cybernétique, COW semble tout à fait raisonnable comme moyen de conserver une syntaxe mutable tout en évitant de copier en profondeur l'intégralité de l'androïde juste pour donner à la copie un bras unique. Le rendre juste immuable en tant que structure de données persistante à ce stade pourrait être supérieur, mais un "COW partiel" appliqué sur des pièces Android individuelles semble raisonnable dans ces cas.
Dans un tel cas, les deux copies de l'androïde partageraient / auraient par exemple le même torse, les jambes, les pieds, la tête, le cou, les épaules, le bassin, etc. Les seules données qui seraient différentes entre elles et non partagées sont le bras qui a été fait unique pour le deuxième androïde sur l'écrasement de son bras.
la source
std::vector<std::string>
avantemplace_back
et à déplacer la sémantique en C ++ 11) . Mais nous utilisons également essentiellement l'instanciation. Le système de nœuds peut ou non modifier les données. Nous avons des choses comme les nœuds pass-through qui ne font rien avec l'entrée mais juste la sortie d'une copie (ils sont là pour l'organisation des utilisateurs de son programme). Dans ces cas, toutes les données sont copiées superficiellement pour les types complexes ...A
est copié et que rien n'est fait pour l'objecterB
, il s'agit d'une copie superficielle bon marché pour les types de données complexes comme les maillages. Maintenant, si nous modifionsB
, les données dans lesquelles nous modifionsB
deviennent uniques via COW, maisA
restent intactes (à l'exception de certains comptages de références atomiques).