Les jours de passage de const std :: string & en tant que paramètre sont-ils terminés?

604

J'ai entendu un discours récent par Herb Sutter qui a laissé entendre que les raisons de passer std::vectoret std::stringpar 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_valsera 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 invalest 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::stringa 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?

Benj
la source
89
Je pense que la meilleure réponse à la question est probablement de lire l' article de Dave Abrahams à ce sujet sur C ++ Next . J'ajouterais que je ne vois rien à ce sujet qui puisse être considéré comme hors sujet ou non constructif. C'est une question claire, concernant la programmation, à laquelle il existe des réponses factuelles.
Jerry Coffin
Fascinant, donc si vous devez faire une copie de toute façon, le passage par valeur est probablement plus rapide que le passage par référence.
Benj
3
@Sz. Je suis sensible aux questions faussement classées comme doublons et fermées. Je ne me souviens pas des détails de cette affaire et je ne les ai pas réexaminés. Au lieu de cela, je vais simplement supprimer mon commentaire en supposant que j'ai fait une erreur. Merci d'avoir attiré mon attention là-dessus.
Howard Hinnant du
2
@HowardHinnant, merci beaucoup, c'est toujours un moment précieux quand on tombe sur ce niveau d'attention et de sensibilité, c'est tellement rafraîchissant! (Je vais supprimer le mien alors, bien sûr.)
Sz.

Réponses:

393

Herb a dit ce qu'il a dit à cause de cas comme celui-ci.

Disons que j'ai une fonction Aqui appelle la fonction B, qui appelle la fonction C. Et Apasse une chaîne à travers Bet dans C. Ane sait pas ou ne se soucie pas C; tout est au Acourant B. Autrement dit, Cest un détail de mise en œuvre de B.

Disons que A est défini comme suit:

void A()
{
  B("value");
}

Si B et C prennent la chaîne const&, cela ressemble à ceci:

void B(const std::string &str)
{
  C(str);
}

void C(const std::string &str)
{
  //Do something with `str`. Does not store it.
}

Très bien. Vous passez juste des pointeurs, pas de copie, pas de mouvement, tout le monde est content. Cprend un const&car il ne stocke pas la chaîne. Il l'utilise simplement.

Maintenant, je veux faire un changement simple: il Cfaut stocker la chaîne quelque part.

void C(const std::string &str)
{
  //Do something with `str`.
  m_str = str;
}

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 Apasse un temporaire; il n'y a aucune raison de Cdevoir 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 Cpour prendre son paramètre par valeur, cela fait juste Bfaire la copie dans ce paramètre; Je ne gagne rien.

Donc, si je venais de passer strpar valeur à travers toutes les fonctions, en comptant sur std::movepour 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?

Nicol Bolas
la source
2
Lorsque vous dites que le passage à une valeur est plus cher que l'utilisation de références, c'est toujours plus cher d'un montant constant (indépendamment de la longueur de la chaîne déplacée), n'est-ce pas?
Neil G
3
@NeilG: Comprenez-vous ce que signifie "dépendant de l'implémentation"? Ce que vous dites est faux, car cela dépend si et comment l' authentification unique est implémentée.
ildjarn
17
@ildjarn: Dans l'analyse de l'ordre, si le pire des cas est lié par une constante, alors c'est toujours un temps constant. N'y a-t-il pas une petite chaîne la plus longue? Cette chaîne ne prend-elle pas un certain temps à copier? Toutes les petites chaînes ne prennent-elles pas moins de temps à copier? Ensuite, la copie de chaînes pour les petites chaînes est "à temps constant" dans l'analyse de l'ordre - malgré le fait que les petites chaînes prennent un temps variable à copier. L'analyse des ordres concerne le comportement asymptotique .
Neil G
8
@NeilG: Bien sûr, mais votre originale question était « qui est encore plus cher d'une quantité constante (indépendante de la longueur de la chaîne étant déplacée) à droite? » Le point que je suis en train de faire est, il pourrait être plus cher par différents des quantités constantes en fonction de la longueur de la chaîne, qui est résumée comme "non".
ildjarn
13
Pourquoi la chaîne serait-elle movedde B à C dans le cas par valeur? Si B est B(std::string b)et C est C(std::string c)alors soit nous devons appeler C(std::move(b))B, soit nous devons brester inchangés (donc «non déplacés») jusqu'à la sortie B. (Peut-être qu'un compilateur d'optimisation déplacera la chaîne sous la règle as-if s'il bn'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 de strto m_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 et std::movedoit se déplacer à partir de cette valeur.
Pixelchemist
163

Les jours de passage de const std :: string & en tant que paramètre sont-ils terminés?

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 passer std::stringpar 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:

Cependant, inval est encore beaucoup plus grand que la taille d'une référence (qui est généralement implémentée en tant que pointeur). Cela est dû au fait qu'une chaîne std :: a divers composants, y compris un pointeur dans le tas et un membre char [] pour une 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?

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.

Justin
la source
3
Lors de l'utilisation de la pile, les ABI typiques transmettront une seule référence dans un registre sans utilisation de pile.
ahcox
63

Cela dépend fortement de l'implémentation du compilateur.

Cependant, cela dépend aussi de ce que vous utilisez.

Examinons les fonctions suivantes:

bool foo1( const std::string v )
{
  return v.empty();
}
bool foo2( const std::string & v )
{
  return v.empty();
}

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, foo2sera plus performant foo1, car foo1fera une copie complète.

Sur mon PC, en utilisant g ++ 4.6.1, j'ai obtenu ces résultats:

  • variable par référence: 1000000000 itérations -> temps écoulé: 2.25912 sec
  • variable par valeur: 1000000000 itérations -> temps écoulé: 27.2259 sec
  • littéral par référence: 100000000 itérations -> temps écoulé: 9.10319 sec
  • littéral par valeur: 100000000 itérations -> temps écoulé: 8.62659 sec
BЈовић
la source
4
Ce qui est plus pertinent, c'est ce qui se passe à l' intérieur de la fonction: aurait-elle, si elle était appelée avec une référence, besoin de faire une copie en interne qui peut être omise lors du passage par valeur ?
leftaroundabout
1
@leftaroundabout Oui, bien sûr. Mon hypothèse que les deux fonctions font exactement la même chose.
BЈовић
5
Ce n'est pas mon point. Que le passage par valeur ou par référence soit préférable dépend de ce que vous faites à l'intérieur de la fonction. Dans votre exemple, vous n'utilisez pas beaucoup de l'objet chaîne, donc la référence est évidemment meilleure. Mais si la tâche de la fonction consistait à placer la chaîne dans une structure ou à exécuter, par exemple, un algorithme récursif impliquant plusieurs divisions de la chaîne, le passage par valeur pourrait en fait économiser une copie, par rapport au passage par référence. Nicol Bolas l'explique assez bien.
leftaroundabout
3
Pour moi, "cela dépend de ce que vous faites à l'intérieur de la fonction" est une mauvaise conception - puisque vous basez la signature de la fonction sur les éléments internes de l'implémentation.
Hans Olsson
1
Peut-être une faute de frappe, mais les deux derniers timings littéraux ont 10 fois moins de boucles.
TankorSmash
54

Réponse courte: NON! Longue réponse:

  • Si vous ne modifiez pas la chaîne (le traitement est en lecture seule), passez-le comme const ref&.
    (le const ref&doit évidemment rester dans la portée pendant que la fonction qui l'utilise s'exécute)
  • Si vous prévoyez de le modifier ou si vous savez qu'il sortira du champ d'application (threads) , passez-le en tant que 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:

Directive : ne copiez pas vos arguments de fonction. Au lieu de cela, passez-les par valeur et laissez le compilateur faire la copie.

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 :

std::string function(const std::string& aString){
    auto vString(aString);
    vString.clear();
    return vString;
}

faites ceci :

std::string function(std::string aString){
    aString.clear();
    return aString;
}

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.

CodeAngry
la source
2
Vous recommandez de passer par référence dans certains cas, mais vous pointez vers une directive qui recommande de toujours passer par valeur.
Keith Thompson
3
@KeithThompson Ne copiez pas vos arguments de fonction. Les moyens ne copient pas le 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.
CodeAngry
5
@KeithThompson La citation du guide (ne copiez pas vos arguments de fonction. Au lieu de cela, passez-les par valeur et laissez le compilateur faire la copie.) Est COPIEE à partir de cette page. Si ce n'est pas assez clair, je ne peux pas m'empêcher. Je ne fais pas entièrement confiance aux compilateurs pour faire les meilleurs choix. Je préfère être très clair sur mes intentions dans la façon dont je définis les arguments de fonction. # 1 Si c'est en lecture seule, c'est a 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 passe ref&. # 4 J'utilise pointers *si un argument est facultatif pour que je puisse le nullptrfaire.
CodeAngry
11
Je ne prends pas parti sur la question de savoir si passer par valeur ou par référence. Mon point est que vous préconisez le passage par référence dans certains cas, mais citez ensuite (apparemment pour soutenir votre position) une ligne directrice qui recommande de toujours passer par valeur. Si vous n'êtes pas d'accord avec la directive, vous voudrez peut-être le dire et expliquer pourquoi. (Les liens vers cpp-next.com ne fonctionnent pas pour moi.)
Keith Thompson
4
@KeithThompson: Vous paraphrasez mal la directive. Il ne s'agit pas de "toujours" passer par valeur. Pour résumer, c'était "Si vous aviez fait une copie locale, utilisez pass by value pour que le compilateur effectue cette copie pour vous." Cela ne veut pas dire d'utiliser une valeur de passage lorsque vous n'allez pas faire de copie.
Ben Voigt
43

Sauf si vous avez réellement besoin d'une copie, il est toujours raisonnable de la prendre const &. Par exemple:

bool isprint(std::string const &s) {
    return all_of(begin(s),end(s),(bool(*)(char))isprint);
}

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.

bames53
la source
2
Les constructeurs de déplacement sont généralement implémentés avec noexcept, mais les constructeurs de copie ne le sont évidemment pas.
leftaroundabout
25

Presque.

En C ++ 17, nous avons basic_string_view<?>, ce qui nous ramène à un cas d'utilisation étroit pour les std::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'une std::stringvaleur par est plus optimale, car vous pouvez movesortir du paramètre.

Si quelqu'un a appelé votre fonction avec un C brut, "string"cela signifie qu'un seul std::stringtampon est jamais alloué, contre deux dans le std::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 des std::stringfonctionnalités similaires sans risquer d'allocation. Une chaîne C brute peut même être transformée en a std::string_viewsans 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 qui std::stringfournissent. Dans la pratique, il s'agit d'un ensemble d'exigences rares.

Yakk - Adam Nevraumont
la source
2
J'apprécie cette réponse - mais je tiens à souligner qu'elle souffre (comme beaucoup de réponses de qualité) d'un peu de biais spécifique au domaine. A savoir: «Dans la pratique, c'est un ensemble d'exigences rares»… dans ma propre expérience de développement, ces contraintes - qui semblent anormalement étroites à l'auteur - sont rencontrées, comme, littéralement tout le temps. Cela vaut la peine de le souligner.
fish2000
1
@ fish2000 Pour être clair, pour std::stringdominer, 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?)
Yakk - Adam Nevraumont
@ Yakk-AdamNevraumont C'est une chose YMMV - mais c'est un cas d'utilisation fréquent si (par exemple) vous programmez contre POSIX, ou d'autres API où la sémantique de la chaîne C est, comme, le plus petit dénominateur commun. Je devrais dire vraiment que j'aime 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.
fish2000
1
@ fish2000 "'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é". En effet, mais cela laisse de côté la meilleure partie - dans le cas où la chaîne brute est un littéral de chaîne, elle ne nécessite même pas de strlen d'exécution () !
Don Hatch
17

std::stringn'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 invalest 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, une constréférence pourrait toujours être la meilleure façon de procéder.

Chiot
la source
2
Point intéressant sur le fait que le constructeur de copie est suffisamment intelligent pour ne pas se soucier du SSO s'il ne l'utilise pas. Probablement correct, je vais devoir vérifier si c'est vrai ;-)
Benj
3
@Benj: Vieux commentaire, je sais, mais si SSO est assez petit, le copier sans condition est plus rapide que de faire une branche conditionnelle. Par exemple, 64 octets est une ligne de cache et peut être copié en un temps vraiment trivial. Probablement 8 cycles ou moins sur x86_64.
Zan Lynx
Même si le SSO n'est pas copié par le constructeur de copie, un 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.
cmaster - réintègre monica le
Oh, et j'ai oublié de parler de passer des arguments de fonction dans les registres; cela ramènerait à zéro l'utilisation de la pile de la référence pour le dernier appelé ...
cmaster - reinstate monica
16

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é:

#include <iostream>

struct string
{
    string() {}
    string(const string&) {std::cout << "string(const string&)\n";}
    string& operator=(const string&) {std::cout << "string& operator=(const string&)\n";return *this;}
#if (__has_feature(cxx_rvalue_references))
    string(string&&) {std::cout << "string(string&&)\n";}
    string& operator=(string&&) {std::cout << "string& operator=(string&&)\n";return *this;}
#endif

};

#if PROCESS == 1

string
do_something(string inval)
{
    // do stuff
    return inval;
}

#elif PROCESS == 2

string
do_something(const string& inval)
{
    string return_val = inval;
    // do stuff
    return return_val; 
}

#if (__has_feature(cxx_rvalue_references))

string
do_something(string&& inval)
{
    // do stuff
    return std::move(inval);
}

#endif

#endif

string source() {return string();}

int main()
{
    std::cout << "do_something with lvalue:\n\n";
    string x;
    string t = do_something(x);
#if (__has_feature(cxx_rvalue_references))
    std::cout << "\ndo_something with xvalue:\n\n";
    string u = do_something(std::move(x));
#endif
    std::cout << "\ndo_something with prvalue:\n\n";
    string v = do_something(source());
}

Pour moi, cela produit:

$ clang++ -std=c++11 -stdlib=libc++ -DPROCESS=1 test.cpp
$ a.out
do_something with lvalue:

string(const string&)
string(string&&)

do_something with xvalue:

string(string&&)
string(string&&)

do_something with prvalue:

string(string&&)
$ clang++ -std=c++11 -stdlib=libc++ -DPROCESS=2 test.cpp
$ a.out
do_something with lvalue:

string(const string&)

do_something with xvalue:

string(string&&)

do_something with prvalue:

string(string&&)

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:

+----+--------+--------+---------+
|    | lvalue | xvalue | prvalue |
+----+--------+--------+---------+
| p1 |  1/1   |  0/2   |   0/1   |
+----+--------+--------+---------+
| p2 |  1/0   |  0/1   |   0/1   |
+----+--------+--------+---------+

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.

Howard Hinnant
la source
1
std :: string est une classe de bibliothèque standard. Il est déjà à la fois mobile et copiable. Je ne vois pas en quoi cela est pertinent. L'OP demande davantage d'informations sur les performances du déplacement par rapport aux références , et non sur les performances du déplacement par rapport à la copie.
Nicol Bolas
3
Cette réponse compte le nombre de déplacements et de copies qu'une chaîne std :: subira sous la conception passe-par-valeur décrite par Herb et Dave, par rapport au passage par référence avec une paire de fonctions surchargées. J'utilise le code de l'OP dans la démo, sauf pour le remplacer par une chaîne factice pour crier quand il est copié / déplacé.
Howard Hinnant
Vous devriez probablement optimiser le code avant d'effectuer les tests…
The Paramagnetic Croissant
3
@TheParamagneticCroissant: Avez-vous obtenu des résultats différents? Si oui, en utilisant quel compilateur avec quels arguments de ligne de commande?
Howard Hinnant
14

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 des const char[N]types, nécessitant une promotion vers std::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 ().)

#include <string>
#include <iostream>
class WidgetBadRef {
public:
    WidgetBadRef(const std::string& s) : myStrRef(s)  // copy the reference...
    {}

    const std::string& myStrRef;    // might be a reference to a temporary (oops!)
};

class WidgetSafeCopy {
public:
    WidgetSafeCopy(const std::string& s) : myStrCopy(s)
            // constructor for string references; copy the string
    {std::cout << "const std::string& constructor\n";}

    WidgetSafeCopy(const char* cs) : myStrCopy(cs)
            // constructor for string literals (and char arrays);
            // for minor efficiency only;
            // create the std::string directly from the chars
    {std::cout << "const char * constructor\n";}

    const std::string myStrCopy;    // save a copy, not a reference!
};

int main() {
    WidgetBadRef w1("First string");
    WidgetSafeCopy w2("Second string"); // uses the const char* constructor, no temp string
    WidgetSafeCopy w3(w2.myStrCopy);    // uses the String reference constructor
    std::cout << w1.myStrRef << "\n";   // garbage out
    std::cout << w2.myStrCopy << "\n";  // OK
    std::cout << w3.myStrCopy << "\n";  // OK
}

PRODUCTION:

const char * constructor
const std::string& constructor

Second string
Second string
circlepi314
la source
C'est un problème différent et WidgetBadRef n'a pas besoin d'avoir un const & paramètre pour se tromper. La question est de savoir si WidgetSafeCopy a simplement pris un paramètre de chaîne, serait-il plus lent? (Je pense que la copie temporaire au membre est certainement plus facile à repérer)
Superfly Jon
Notez que ce 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.
Justin Time - Rétablir Monica le
@JustinTime: merci; J'ai supprimé la dernière phrase incorrecte affirmant, en effet, que std :: string && serait une référence universelle.
circlepi314
@ circlepi314 Vous êtes les bienvenus. C'est un mélange facile à faire, il peut parfois être difficile de savoir si une donnée donnée 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.
Justin Time - Reinstate Monica
8

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:

  1. Si vous écrivez tout le code de l'extérieur vers les fonctions intérieures, vous savez ce que fait le code, vous pouvez utiliser la référence const std::string &.
  2. Si vous écrivez le code de bibliothèque ou utilisez beaucoup de code de bibliothèque là où des chaînes sont passées, vous gagnerez probablement plus au sens global en faisant confiance au std::stringcomportement du constructeur de copie.
digital_infinity
la source
6

Voir «Herb Sutter« Retour aux bases! Les bases du style C ++ moderne » . idée de passer des chaînes par valeur.

diapo 24

Les benchmarks montrent que le passage de std::strings 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.

JDługosz
la source
3

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 fqui 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 farguments lvalue et rvalue respectivement. Pour voir pourquoi c'est le cas, supposons fprend un paramètre de valeur, où Test un type constructible de copie et de déplacement:

void f(T x) {
  T y{std::move(x)};
}

L'appel favec un argument lvalue entraînera un constructeur de copie appelé pour construire xet un constructeur de mouvement appelé pour construire y. D'un autre côté, l'appel favec un argument rvalue entraînera l' appel d'un constructeur de mouvement pour construire xet celui d'un autre constructeur de déplacement pour construire y.

En général, l'implémentation optimale des farguments for lvalue est la suivante:

void f(const T& x) {
  T y{x};
}

Dans ce cas, un seul constructeur de copie est appelé pour construire y. L'implémentation optimale des farguments for rvalue est, encore une fois en général, la suivante:

void f(T&& x) {
  T y{std::move(x)};
}

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 fqui 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 fonction fqui conserve une copie de son paramètre aura la forme:

void f(T x) {
  T y{...};
  ...
  y = std::move(x);
}

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:

void f(const T& x) {
  T y{...};
  ...
  y = x;
}

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 dansy , 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 fcelle qui conserve une copie a la forme:

void f(T&& x) {
  T y{...};
  ...
  y = std::move(x);
}

Donc, seulement une affectation de déplacement dans ce cas. Passer une valeur r à la version fqui 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 fcas où vous opteriez pour une surcharge sur la catégorie de valeur de l'argument. Le transfert parfait a l'inconvénient de fdevenir 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).

Ton van den Heuvel
la source
Voir les découvertes de Herb Sutter dans ma nouvelle réponse: ne faites cela que lorsque vous déplacez la construction , pas le déplacement.
JDługosz
1
@ JDługosz, merci pour le pointeur du discours de Herb, je viens de le regarder et j'ai complètement révisé ma réponse. Je n'étais pas au courant du (déménagement) attribuer des conseils.
Ton van den Heuvel
ce chiffre et ces conseils figurent maintenant dans le document des directives standard .
JDługosz
1

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.

Erik Aronesty
la source
@BenVoigt yep ... au lieu de le supprimer, il devrait être mutable ... mais vous ne pouvez pas changer un membre STL en mutable dans une classe dérivée.
Erik Aronesty