Question Les jours de passage de const std :: string & comme paramètre sont-ils terminés?


J'ai entendu un récent discours de Herb Sutter qui a suggéré que les raisons de passer std::vector et std::string par const & sont largement partis. Il a suggéré que l'écriture d'une fonction telle que la suivante est maintenant préférable:

std::string do_something ( std::string inval )
{
   std::string return_val;
   // ... do stuff ...
   return return_val;
}

Je comprends que le return_val sera une valeur au point où la fonction retourne et peut donc être retournée en utilisant la sémantique de déplacement, qui est très bon marché. cependant, inval est toujours beaucoup plus grande que la taille d'une référence (qui est généralement implémentée comme un pointeur). C'est parce qu'un 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 le passage par référence reste une bonne idée.

Quelqu'un peut-il expliquer pourquoi Herb aurait pu dire cela?


513
2018-04-19 15:20


origine


Réponses:


La raison pour laquelle Herb a dit ce qu'il a dit est à cause de cas comme celui-ci.

Disons que j'ai la fonction A qui appelle la fonction B, qui appelle la fonction C. Et A passe une chaîne à travers B et dans C. A ne sait pas ou se soucient de C; tout A sait à propos de B. C'est, C est 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 par const&, alors ça ressemble à ceci:

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

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

Tout va bien. Vous passez juste des pointeurs, pas de copie, pas de mouvement, tout le monde est content. C prend un const& parce qu'il ne stocke pas la chaîne. Il l'utilise simplement.

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

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

Bonjour, copiez le constructeur et l'allocation de mémoire potentielle (ignorez le Optimisation de chaîne courte (SSO)). La sémantique de déplacement de C ++ 11 est supposée permettre de supprimer la construction de copie inutile, n'est-ce pas? Et A passe un temporaire; il n'y a aucune raison pour laquelle C devrait devoir copie les données. Il devrait simplement s'enfuir avec ce qui lui a été donné.

Sauf que ça ne peut pas. Parce qu'il faut un const&.

Si je change C prendre son paramètre par valeur, cela ne fait que provoquer B faire la copie dans ce paramètre; Je ne gagne rien.

Donc, si je venais de passer str par la valeur à travers toutes les fonctions, en s'appuyant sur std::move pour mélanger les données, nous n'aurions pas ce problème. Si quelqu'un veut s'y accrocher, ils peuvent le faire. Si ce n'est pas le cas, 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. Cela vaut-il la peine de le faire?

Cela dépend de votre cas d'utilisation. Combien détestez-vous les allocations de mémoire?


349
2018-04-19 16:41



Les jours de passage de const std :: string & comme 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 à tout  std::string paramètres -- Toujours qui passe std::string La valeur n'est pas une «meilleure pratique» pour tous les paramètres et applications arbitraires, car les optimisations sur lesquelles ces articles / articles se concentrent s'appliquent seulement à un ensemble restreint de cas.

Si vous renvoyez une valeur, que vous modifiez le paramètre ou que vous en prenez la valeur, le fait de passer en valeur pourrait vous éviter une copie coûteuse et vous offrir une commodité syntaxique.

Comme toujours, le fait de passer par la référence const sauve beaucoup de copies quand vous n'avez pas besoin d'une copie.

Maintenant à l'exemple spécifique:

Cependant, inval est encore beaucoup plus volumineux que la taille d'une référence (qui est généralement implémentée en tant que pointeur). En effet, une chaîne std :: a plusieurs composants dont un pointeur dans le tas et un membre char [] pour l'optimisation des chaînes courtes. Il me semble donc que le passage par référence reste une bonne idée. Quelqu'un peut-il expliquer pourquoi Herb aurait pu dire cela?

Si la taille de la pile est préoccupante (et en supposant qu'elle ne soit pas inline / optimisée), return_val + inval > return_val - IOW, l'utilisation maximale de la pile peut être réduit en passant par valeur ici (note: 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 où c'est applicable.

Les jours de passage par référence const ne sont pas terminés - les règles sont simplement plus compliquées qu'elles ne l'étaient autrefois. Si les performances sont importantes, il est conseillé de réfléchir à la manière dont vous passez ces types, en fonction des détails que vous utilisez dans vos implémentations.


125
2018-04-19 21:07



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

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

Considérons les prochaines fonctions:

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 les inlining. Alors :
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 surperformera foo1, car foo1 fera une copie profonde.

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 la 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

55
2018-04-19 15:44



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

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

Si vous modifiez cela pour prendre la chaîne en valeur, vous finirez par déplacer ou copier le paramètre, ce qui n'est pas nécessaire. Non seulement la copie / déménagement est probablement plus coûteuse, mais elle introduit également une nouvelle défaillance potentielle; la copie / le déplacement peut déclencher une exception (par exemple, l'attribution pendant la copie peut échouer) alors qu'une prise de référence à une valeur existante ne peut pas l'être.

Si vous faire besoin d'une copie, puis en passant et en revenant en valeur est généralement (toujours?) la meilleure option. En fait, je ne m'inquiéterais généralement pas de cela en C ++ 03, sauf si vous trouvez que des copies supplémentaires causent effectivement un problème de performance. Copie elision semble assez fiable sur les compilateurs modernes. Je pense que le scepticisme des gens et l'insistance que vous devez vérifier votre table de support de compilateur pour RVO est pour la plupart obsolète de nos jours.


En résumé, C ++ 11 ne change rien à cet égard, sauf pour les personnes qui ne font pas confiance à l’élision des copies.


41
2018-04-19 15:29



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

  • Si vous ne modifiez pas la chaîne (traiter est en lecture seule), transmettez-la const ref&.
    (la const ref& doit de toute évidence rester dans la portée pendant que la fonction qui l'utilise s'exécute)
  • Si vous envisagez de le modifier ou si vous savez qu'il sera hors de portée (threads), le passer comme un value, ne copiez pas le const ref& à l'intérieur de votre corps de fonction.

Il y avait un post sur cpp-next.com appelé "Vous voulez la vitesse, passez en valeur!". Le TL; DR:

Ligne directrice: 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 pas copier vos arguments de fonction --- veux dire: Si vous envisagez de modifier la valeur de l'argument en la copiant dans une variable interne, utilisez plutôt un argument de valeur.

Alors, ne fais pas ça:

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

fais ceci:

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

Lorsque vous avez besoin de modifier la valeur de l'argument dans votre corps de fonction.

Vous devez simplement savoir comment vous envisagez d'utiliser l'argument dans le corps de la fonction. Lecture seule ou NON ... et si elle reste dans la portée.


40
2017-08-23 16:33



std::string n'est pas Données anciennes (POD), et sa taille brute n'est pas la chose la plus pertinente. Par exemple, si vous passez une chaîne qui est au-dessus de 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 que 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 performance, en supposant que vous ayez besoin de la propriété de l'argument. Si vous ne le faites pas, un const référence pourrait encore être la meilleure façon d'y aller.


16
2018-04-19 15:24



Presque.

En C ++ 17, nous avons basic_string_view<?>, ce qui nous amène à essentiellement un cas d'utilisation étroite pour std::string const& paramètres.

L'existence de la sémantique du mouvement a éliminé un cas d'utilisation std::string const& - Si vous envisagez de stocker le paramètre, prenez une std::string en valeur est plus optimale, comme vous le pouvez move hors du paramètre.

Si quelqu'un a appelé votre fonction avec un C brut "string" cela signifie seulement un std::string tampon est jamais attribué, par opposition à deux dans le std::string const& Cas.

Cependant, si vous n'avez pas l'intention de faire une copie, en prenant std::string const& est toujours utile en C ++ 14.

Avec std::string_view, tant que vous ne transmettez pas cette chaîne à une API qui attend un style C '\0'tampons de caractères terminés, vous pouvez obtenir plus efficacement std::string comme fonctionnalité sans risquer une allocation. Une chaîne C brute peut même être transformée en std::string_view sans aucune allocation ou copie de caractères.

A ce stade, l'utilisation de std::string const& est lorsque vous ne copiez pas les données en gros, et vont le transmettre à une API de style C qui attend un tampon terminé nul, et vous avez besoin des fonctions de chaîne de niveau supérieur qui std::stringfournit. En pratique, il s’agit d’un ensemble rare d’exigences.


16
2018-01-26 03:09



J'ai copié / collé la réponse de cette question ici, et a changé les noms et l'orthographe pour répondre à 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 sort:

$ 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 second nombre est le nombre de constructions de mouvement:

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

La solution par valeur ne nécessite qu'une seule surcharge, mais coûte une construction de déplacement supplémentaire lors du passage de lvalues ​​et de xvalues. Cela peut ou peut ne pas être acceptable pour une situation donnée. Les deux solutions ont des avantages et des inconvénients.


15
2018-04-19 16:13