J'ai entendu un discours récent par Herb Sutter qui a laissé entendre que les raisons de passer std::vector
et std::string
par const &
sont en grande partie disparu. Il suggère qu'il est désormais préférable d'écrire une fonction telle que la suivante:
std::string do_something ( std::string inval )
{
std::string return_val;
// ... do stuff ...
return return_val;
}
Je comprends que la valeur return_val
sera une valeur r au moment où la fonction retourne et peut donc être renvoyée en utilisant la sémantique de déplacement, qui est très bon marché. Cependant, il inval
est encore beaucoup plus grand que la taille d'une référence (qui est généralement implémentée comme pointeur). Cela est dû au fait que a std::string
a divers composants, y compris un pointeur dans le tas et un membre char[]
pour l'optimisation de chaîne courte. Il me semble donc que passer par référence est toujours une bonne idée.
Quelqu'un peut-il expliquer pourquoi Herb aurait pu dire cela?
Réponses:
Herb a dit ce qu'il a dit à cause de cas comme celui-ci.
Disons que j'ai une fonction
A
qui appelle la fonctionB
, qui appelle la fonctionC
. EtA
passe une chaîne à traversB
et dansC
.A
ne sait pas ou ne se soucie pasC
; tout est auA
courantB
. Autrement dit,C
est un détail de mise en œuvre deB
.Disons que A est défini comme suit:
Si B et C prennent la chaîne
const&
, cela ressemble à ceci:Très bien. Vous passez juste des pointeurs, pas de copie, pas de mouvement, tout le monde est content.
C
prend unconst&
car il ne stocke pas la chaîne. Il l'utilise simplement.Maintenant, je veux faire un changement simple: il
C
faut stocker la chaîne quelque part.Bonjour, copier le constructeur et l'allocation de mémoire potentielle (ignorer le Short String Optimization (SSO) ). La sémantique de mouvement de C ++ 11 est censée permettre de supprimer la construction de copie inutile, non? Et
A
passe un temporaire; il n'y a aucune raison deC
devoir copier les données. Il devrait simplement prendre la fuite avec ce qui lui a été donné.Sauf que ce n'est pas possible. Parce qu'il faut un
const&
.Si je change
C
pour prendre son paramètre par valeur, cela fait justeB
faire la copie dans ce paramètre; Je ne gagne rien.Donc, si je venais de passer
str
par valeur à travers toutes les fonctions, en comptant surstd::move
pour mélanger les données, nous n'aurions pas ce problème. Si quelqu'un veut s'y accrocher, il le peut. S'ils ne le font pas, eh bien.Est-ce plus cher? Oui; passer à une valeur est plus cher que d'utiliser des références. Est-ce moins cher que la copie? Pas pour les petites chaînes avec SSO. Vaut-il la peine?
Cela dépend de votre cas d'utilisation. Combien détestez-vous les allocations de mémoire?
la source
moved
de B à C dans le cas par valeur? Si B estB(std::string b)
et C estC(std::string c)
alors soit nous devons appelerC(std::move(b))
B, soit nous devonsb
rester inchangés (donc «non déplacés») jusqu'à la sortieB
. (Peut-être qu'un compilateur d'optimisation déplacera la chaîne sous la règle as-if s'ilb
n'est pas utilisé après l'appel, mais je ne pense pas qu'il y ait une forte garantie.) Il en va de même pour la copie destr
tom_str
. Même si un paramètre de fonction a été initialisé avec une valeur r, il s'agit d'une valeur l à l'intérieur de la fonction etstd::move
doit se déplacer à partir de cette valeur.Non . Beaucoup de gens prennent ce conseil (y compris Dave Abrahams) au-delà du domaine auquel il s'applique, et le simplifient pour s'appliquer à tous les
std::string
paramètres - Toujours passerstd::string
par valeur n'est pas une "meilleure pratique" pour tous les paramètres et applications arbitraires car les optimisations ces les discussions / articles se concentrent uniquement sur un ensemble restreint de cas .Si vous renvoyez une valeur, modifiez le paramètre ou prenez la valeur, le fait de passer par la valeur peut économiser une copie coûteuse et offrir une commodité syntaxique.
Comme toujours, le passage par la référence const permet d'économiser beaucoup de copie lorsque vous n'en avez pas besoin .
Passons maintenant à l'exemple spécifique:
Si la taille de la pile est un problème (et en supposant que cela ne soit pas intégré / optimisé),
return_val
+inval
>return_val
- IOW, l'utilisation maximale de la pile peut être réduite en passant ici une valeur (remarque: simplification excessive des ABI). Pendant ce temps, le passage par la référence const peut désactiver les optimisations. La raison principale ici n'est pas d'éviter la croissance de la pile, mais de s'assurer que l'optimisation peut être effectuée là où elle est applicable .Les jours de passage par référence const ne sont pas terminés - les règles sont plus compliquées qu'elles ne l'étaient autrefois. Si les performances sont importantes, vous serez avisé de déterminer comment passer ces types, en fonction des détails que vous utilisez dans vos implémentations.
la source
Cela dépend fortement de l'implémentation du compilateur.
Cependant, cela dépend aussi de ce que vous utilisez.
Examinons les fonctions suivantes:
Ces fonctions sont implémentées dans une unité de compilation séparée afin d'éviter l'inline. Ensuite:
1. Si vous passez un littéral à ces deux fonctions, vous ne verrez pas beaucoup de différence dans les performances. Dans les deux cas, un objet chaîne doit être créé
2. Si vous passez un autre objet std :: string,
foo2
sera plus performantfoo1
, carfoo1
fera une copie complète.Sur mon PC, en utilisant g ++ 4.6.1, j'ai obtenu ces résultats:
la source
Réponse courte: NON! Longue réponse:
const ref&
.(le
const ref&
doit évidemment rester dans la portée pendant que la fonction qui l'utilise s'exécute)value
, ne copiez pas l'const ref&
intérieur de votre corps de fonction.Il y avait un article sur cpp-next.com intitulé "Want speed, pass by value!" . Le TL; DR:
TRADUCTION de ^
Ne copiez pas vos arguments de fonction --- signifie: si vous prévoyez de modifier la valeur de l'argument en la copiant dans une variable interne, utilisez simplement un argument de valeur à la place .
Alors, ne faites pas ça :
faites ceci :
Lorsque vous devez modifier la valeur d'argument dans votre corps de fonction.
Vous devez simplement savoir comment vous prévoyez d'utiliser l'argument dans le corps de la fonction. En lecture seule ou NON ... et si cela reste dans la portée.
la source
const ref&
dans une variable interne pour le modifier. Si vous devez le modifier ... faites du paramètre une valeur. C'est assez clair pour moi non anglophone.const ref&
. # 2 Si j'ai besoin de l'écrire ou si je sais qu'il sort du cadre ... j'utilise une valeur. # 3 Si je dois modifier la valeur d'origine, je passeref&
. # 4 J'utilisepointers *
si un argument est facultatif pour que je puisse lenullptr
faire.Sauf si vous avez réellement besoin d'une copie, il est toujours raisonnable de la prendre
const &
. Par exemple:Si vous changez cela pour prendre la chaîne par valeur, vous finirez par déplacer ou copier le paramètre, et cela n'est pas nécessaire. Non seulement le copier / déplacer est probablement plus cher, mais il introduit également une nouvelle défaillance potentielle; la copie / le déplacement peut lever une exception (par exemple, l'allocation pendant la copie peut échouer) alors que la prise d'une référence à une valeur existante ne peut pas.
Si vous avez besoin d' une copie puis passage et le retour en valeur est généralement (toujours?) La meilleure option. En fait, je ne m'inquiéterais généralement pas à ce sujet en C ++ 03 à moins que vous ne constatiez que des copies supplémentaires causent réellement un problème de performances. L'élision de copie semble assez fiable sur les compilateurs modernes. Je pense que le scepticisme et l'insistance des gens que vous devez vérifier votre table de prise en charge du compilateur pour RVO est aujourd'hui largement obsolète.
En bref, C ++ 11 ne change vraiment rien à cet égard, sauf pour les personnes qui ne font pas confiance à l'élision de copie.
la source
noexcept
, mais les constructeurs de copie ne le sont évidemment pas.Presque.
En C ++ 17, nous avons
basic_string_view<?>
, ce qui nous ramène à un cas d'utilisation étroit pour lesstd::string const&
paramètres.L'existence d'une sémantique de déplacement a éliminé un cas d'utilisation pour
std::string const&
- si vous prévoyez de stocker le paramètre, la prise d'unestd::string
valeur par est plus optimale, car vous pouvezmove
sortir du paramètre.Si quelqu'un a appelé votre fonction avec un C brut,
"string"
cela signifie qu'un seulstd::string
tampon est jamais alloué, contre deux dans lestd::string const&
cas.Cependant, si vous n'avez pas l'intention de faire une copie, la prise en charge
std::string const&
est toujours utile en C ++ 14.Avec
std::string_view
, tant que vous ne transmettez pas ladite chaîne à une API qui attend'\0'
des tampons de caractères terminés de style C , vous pouvez obtenir plus efficacement desstd::string
fonctionnalités similaires sans risquer d'allocation. Une chaîne C brute peut même être transformée en astd::string_view
sans aucune allocation ni copie de caractères.À ce stade, l'utilisation de
std::string const&
est lorsque vous ne copiez pas les données en gros et que vous les transmettez à une API de style C qui attend un tampon terminé par null, et vous avez besoin des fonctions de chaîne de niveau supérieur quistd::string
fournissent. Dans la pratique, il s'agit d'un ensemble d'exigences rares.la source
std::string
dominer, vous n'avez pas seulement besoin de certaines de ces exigences, mais de toutes . J'admets que l'un ou même deux d'entre eux sont courants. Peut-être avez-vous généralement besoin des 3 (par exemple, vous faites une analyse syntaxique d'un argument de chaîne pour choisir à quelle API C vous allez le passer en gros?)std::string_view
- comme vous le faites remarquer, "Une chaîne C brute peut même être transformée en std :: string_view sans aucune allocation ni copie de caractère", ce qui mérite d'être rappelé à ceux qui utilisent C ++ dans le contexte de une telle utilisation de l'API, en effet.std::string
n'est pas Plain Old Data (POD) , et sa taille brute n'est pas la chose la plus pertinente jamais. Par exemple, si vous passez une chaîne qui est supérieure à la longueur de SSO et allouée sur le tas, je m'attendrais à ce que le constructeur de copie ne copie pas le stockage SSO.La raison pour laquelle cela est recommandé est parce qu'il
inval
est construit à partir de l'expression d'argument, et est donc toujours déplacé ou copié comme il convient - il n'y a pas de perte de performances, en supposant que vous ayez besoin de la propriété de l'argument. Si vous ne le faites pas, uneconst
référence pourrait toujours être la meilleure façon de procéder.la source
std::string<>
est de 32 octets qui sont alloués à partir de la pile, dont 16 doivent être initialisés. Comparez cela à seulement 8 octets alloués et initialisés pour une référence: c'est deux fois plus de travail CPU, et cela prend quatre fois plus d'espace de cache qui ne sera pas disponible pour d'autres données.J'ai copié / collé la réponse de cette question ici, et changé les noms et l'orthographe pour l'adapter à cette question.
Voici le code pour mesurer ce qui est demandé:
Pour moi, cela produit:
Le tableau ci-dessous résume mes résultats (en utilisant clang -std = c ++ 11). Le premier nombre est le nombre de constructions de copie et le deuxième nombre est le nombre de constructions de déplacement:
La solution passe-par-valeur ne nécessite qu'une seule surcharge mais coûte une construction de déplacement supplémentaire lors du passage des valeurs l et x. Cela peut être acceptable ou non pour une situation donnée. Les deux solutions présentent des avantages et des inconvénients.
la source
Herb Sutter est toujours enregistré, avec Bjarne Stroustroup, en recommandant
const std::string&
comme type de paramètre; voir https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Rf-in .Il y a un piège qui n'est mentionné dans aucune des autres réponses ici: si vous passez une chaîne littérale à un
const std::string&
paramètre, il passera une référence à une chaîne temporaire, créée à la volée pour contenir les caractères du littéral. Si vous enregistrez ensuite cette référence, elle sera invalide une fois la chaîne temporaire désallouée. Pour être sûr, vous devez enregistrer une copie , pas la référence. Le problème vient du fait que les littéraux de chaîne sont desconst char[N]
types, nécessitant une promotion versstd::string
.Le code ci-dessous illustre l'écueil et la solution de contournement, ainsi qu'une option d'efficacité mineure - la surcharge avec une
const char*
méthode, comme décrit dans Existe-t-il un moyen de passer un littéral de chaîne comme référence en C ++ .(Remarque: Sutter & Stroustroup conseille que si vous conservez une copie de la chaîne, fournissez également une fonction surchargée avec un paramètre && et std :: move ().)
PRODUCTION:
la source
T&&
n'est pas toujours une référence universelle; en fait,std::string&&
sera toujours une référence rvalue, et jamais une référence universelle, car aucune déduction de type n'est effectuée . Ainsi, les conseils de Stroustroup & Sutter ne sont pas en conflit avec ceux de Meyers.T&&
est une référence universelle déduite ou une référence de valeur non déduite; cela aurait probablement été plus clair s'ils avaient introduit un symbole différent pour les références universelles (comme&&&
, comme une combinaison de&
et&&
), mais cela aurait probablement l'air idiot.IMO utilisant la référence C ++ pour
std::string
est une optimisation locale rapide et courte, tandis que l'utilisation du passage par valeur pourrait être (ou non) une meilleure optimisation globale.La réponse est donc: cela dépend des circonstances:
const std::string &
.std::string
comportement du constructeur de copie.la source
Voir «Herb Sutter« Retour aux bases! Les bases du style C ++ moderne » . idée de passer des chaînes par valeur.
Les benchmarks montrent que le passage de
std::string
s par valeur, dans les cas où la fonction le copiera de toute façon, peut être considérablement plus lent!C'est parce que vous le forcez à toujours faire une copie complète (puis à vous déplacer), tandis que la
const&
version mettra à jour l'ancienne chaîne qui peut réutiliser le tampon déjà alloué.Voir sa diapositive 27: Pour les fonctions «set», l'option 1 est la même que d'habitude. L'option 2 ajoute une surcharge pour la référence rvalue, mais cela donne une explosion combinatoire s'il y a plusieurs paramètres.
Ce n'est que pour les paramètres «puits» où une chaîne doit être créée (sans que sa valeur existante soit modifiée) que l'astuce passe-par-valeur soit valide. Autrement dit, les constructeurs dans lesquels le paramètre initialise directement le membre du type correspondant.
Si vous voulez voir à quel point vous pouvez vous inquiéter à ce sujet, regardez la présentation de Nicolai Josuttis et bonne chance avec cela ( "Perfect - Done!" N fois après avoir trouvé la faute à la version précédente. Avez-vous déjà été là?)
Ceci est également résumé comme ⧺F.15 dans les directives standard.
la source
Comme @ JDługosz le souligne dans les commentaires, Herb donne d'autres conseils dans une autre (plus tard?) Conférence, voir à peu près d'ici: https://youtu.be/xnqTKD8uD64?t=54m50s .
Son conseil se résume à n'utiliser que des paramètres de valeur pour une fonction
f
qui prend ce qu'on appelle des arguments puits, en supposant que vous déplacerez la construction à partir de ces arguments puits.Cette approche générale n'ajoute que la surcharge d'un constructeur de déplacement pour les arguments lvalue et rvalue par rapport à une implémentation optimale des
f
arguments lvalue et rvalue respectivement. Pour voir pourquoi c'est le cas, supposonsf
prend un paramètre de valeur, oùT
est un type constructible de copie et de déplacement:L'appel
f
avec un argument lvalue entraînera un constructeur de copie appelé pour construirex
et un constructeur de mouvement appelé pour construirey
. D'un autre côté, l'appelf
avec un argument rvalue entraînera l' appel d'un constructeur de mouvement pour construirex
et celui d'un autre constructeur de déplacement pour construirey
.En général, l'implémentation optimale des
f
arguments for lvalue est la suivante:Dans ce cas, un seul constructeur de copie est appelé pour construire
y
. L'implémentation optimale desf
arguments for rvalue est, encore une fois en général, la suivante:Dans ce cas, un seul constructeur de déplacement est appelé pour construire
y
.Un compromis raisonnable consiste donc à prendre un paramètre de valeur et à demander à un constructeur de déplacement supplémentaire d'appeler les arguments lvalue ou rvalue par rapport à l'implémentation optimale, ce qui est également le conseil donné dans le discours de Herb.
Comme @ JDługosz l'a souligné dans les commentaires, le passage par valeur n'a de sens que pour les fonctions qui construiront un objet à partir de l'argument puits. Lorsque vous avez une fonction
f
qui copie son argument, l'approche passe-par-valeur aura plus de surcharge qu'une approche générale passe-par-const-référence. L'approche passe-par-valeur pour une fonctionf
qui conserve une copie de son paramètre aura la forme:Dans ce cas, il existe une construction de copie et une affectation de déplacement pour un argument lvalue, ainsi qu'une construction de déplacement et une affectation de déplacement pour un argument rvalue. Le cas le plus optimal pour un argument lvalue est:
Cela se résume à une affectation uniquement, ce qui est potentiellement beaucoup moins cher que le constructeur de copie plus l'affectation de déplacement requise pour l'approche de passage par valeur. La raison en est que l'affectation peut réutiliser la mémoire allouée existante dans
y
, et donc empêcher les (dé) allocations, tandis que le constructeur de copie alloue généralement de la mémoire.Pour un argument rvalue, l'implémentation la plus optimale pour
f
celle qui conserve une copie a la forme:Donc, seulement une affectation de déplacement dans ce cas. Passer une valeur r à la version
f
qui prend une référence const ne coûte qu'une affectation au lieu d'une affectation de déplacement. Donc, relativement parlant, la version def
prendre une référence const dans ce cas comme l'implémentation générale est préférable.Donc, en général, pour la mise en œuvre la plus optimale, vous devrez surcharger ou faire une sorte de transfert parfait, comme indiqué dans l'exposé. L'inconvénient est une explosion combinatoire du nombre de surcharges requises, en fonction du nombre de paramètres pour le
f
cas où vous opteriez pour une surcharge sur la catégorie de valeur de l'argument. Le transfert parfait a l'inconvénient def
devenir une fonction de modèle, ce qui empêche de le rendre virtuel et entraîne un code beaucoup plus complexe si vous voulez le faire à 100% (voir le discours pour les détails sanglants).la source
Le problème est que "const" est un qualificatif non granulaire. Ce que l'on entend généralement par «const string ref» est «ne modifiez pas cette chaîne», et non «ne modifiez pas le nombre de références». Il n'y a tout simplement aucun moyen, en C ++, de dire quels membres sont "const". Ils le sont tous, ou aucun ne l'est.
Afin de contourner ce problème de langage, STL pourrait autoriser "C ()" dans votre exemple à faire une copie sémantique de mouvement de toute façon , et ignorer consciencieusement le "const" en ce qui concerne le nombre de références (mutable). Tant qu'il était bien spécifié, ce serait bien.
Puisque STL ne le fait pas, j'ai une version d'une chaîne qui const_casts <> loin du compteur de référence (aucun moyen de rendre rétroactivement quelque chose de mutable dans une hiérarchie de classes), et - et voilà - vous pouvez librement passer les cmstring comme références const, et en faire des copies dans des fonctions approfondies, toute la journée, sans fuites ni problèmes.
Puisque C ++ n'offre aucune "granularité const de classe dérivée" ici, écrire une bonne spécification et créer un nouvel objet "chaîne mobile const" (cmstring) est la meilleure solution que j'ai vue.
la source