web-dev-qa-db-fra.com

Est-ce que les jours de passage de const std :: string & en tant que paramètre sont passés?

J'ai récemment entendu un discours de Herb Sutter suggérer que les raisons de passer std::vector et std::string par const & ont pratiquement disparu. Il a suggéré d’écrire une fonction comme celle-ci:

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 rvalue au point de retour de la fonction et peut donc être renvoyé à l’aide de la sémantique du déplacement, qui est très économique. Cependant, inval est toujours beaucoup plus grand que la taille d'une référence (qui est généralement implémentée sous forme de pointeur). En effet, un std::string comporte divers composants, notamment un pointeur dans le segment de mémoire et un membre char[] pour l'optimisation de chaînes courtes. 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?

572
Benj

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 connaît ni ne se soucie de C; tout ce que A sait sur est B. C'est-à-dire que C est un détail d'implémentation 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 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.
}

Tout va bien. Vous ne faites que passer 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 simple changement: 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 Short String Optimization (SSO) ). La sémantique de déplacement de C++ 11 est censé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 que C doive copier les données. Il devrait juste échapper à ce qui lui a été donné.

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

Si je modifie C pour prendre son paramètre par valeur, cela provoque simplement que B effectue la copie dans ce paramètre; Je ne gagne rien.

Donc, si je venais de passer str par valeur à travers toutes les fonctions, en m'appuyant sur std::move pour mélanger les données, nous n'aurions pas ce problème. Si quelqu'un veut s'y accrocher, il le peut. Si ce n'est pas le cas, eh bien.

Est-ce plus cher? Oui; passer à une valeur coûte 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 d'être fait?

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

379
Nicol Bolas

Est-ce que les jours de passage de const std :: string & en tant que paramètre sont passés?

Non. Beaucoup de gens suivent ce conseil (y compris Dave Abrahams) au-delà du domaine auquel il s'applique et le simplifient pour l'appliquer à all std::string paramètres - Toujours passer std::string par valeur n'est pas une "pratique recommandée" pour tous les paramètres et applications arbitraires, car les optimisations sont centrées sur les entretiens/articles. on apply uniquement à un ensemble restreint de cas .

Si vous renvoyez une valeur, modifiez le paramètre ou prenez la valeur, le fait de passer d'une valeur à une autre peut vous faire économiser des copies coûteuses et offrir une commodité syntaxique.

Comme toujours, passer par la référence const économise beaucoup de copie lorsque vous n'avez pas besoin de copie .

Passons maintenant à l'exemple spécifique:

Cependant, inval est toujours beaucoup plus grand que la taille d'une référence (qui est généralement implémentée sous forme de pointeur). Cela est dû au fait qu'un std :: string comporte divers composants, notamment un pointeur dans le segment de mémoire 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 aligné ou optimisé), return_val + inval> return_val - IOW, l'utilisation maximale de la pile peut être réduit en passant par valeur ici (remarque: simplification excessive des ABI). Pendant ce temps, passer par référence const peut désactiver les optimisations. La principale raison ici n'est pas d'éviter la croissance de la pile, mais de s'assurer que l'optimisation peut être effectuée lorsqu'elle est applicable .

Les jours de référence constante ne sont pas terminés - les règles sont plus compliquées qu'elles ne l'étaient auparavant. Si les performances sont importantes, il est judicieux de déterminer comment vous transmettez ces types, en fonction des détails que vous utilisez dans vos implémentations.

150
justin

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

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

Permet d’envisager 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 distincte afin d’éviter l’alignement. Ensuite :
1. Si vous passez littéralement à 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 transmettez un autre objet std :: string, foo2 surperformera foo1, car foo1 effectuera une copie complète.

Sur mon PC, en utilisant g ++ 4.6.1, j'ai eu 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 en valeur: 100000000 itérations -> temps écoulé: 8.62659 sec
60
BЈовић

Réponse courte: NON! Réponse longue:

  • Si vous ne modifiez pas la chaîne (le traitement est en lecture seule), transmettez-le sous la forme const ref&.
    (le const ref& doit évidemment rester dans le champ d'application pendant l'exécution de la fonction qui l'utilise)
  • Si vous envisagez de le modifier ou si vous savez qu'il sera hors de portée (threads) , transmettez-le en tant que value, ne copiez pas le const ref& dans votre corps de fonction

Il y avait un message sur cpp-next.com appelé "Voulez-vous de la vitesse, passez par valeur! " . Le TL; DR:

Instruction : Ne copiez pas les arguments de votre fonction. À la place, transmettez-les par valeur et laissez le compilateur faire la copie.

TRADUCTION de ^

Ne copiez pas les arguments de votre fonction --- signifie: si vous prévoyez de modifier la valeur de l'argument en le copiant dans un variable interne, utilisez simplement un argument de valeur à la place .

Donc, ne faites pas ceci :

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

faire ceci :

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

Lorsque vous devez modifier la valeur de l'argument dans le corps de votre fonction.

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

47
CodeAngry

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 par valeur, vous finirez par déplacer ou copier le paramètre, ce qui est inutile. Copier/déplacer est non seulement plus coûteux, mais il introduit également une nouvelle défaillance potentielle. la copie/le déplacement peut générer une exception (par exemple, l’affectation lors de la copie peut échouer) alors que le fait de faire référence à une valeur existante ne le peut pas.

Si vous avez avez besoin d'une copie, alors passer et retourner par 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 constatiez que des copies supplémentaires entraînent un problème de performances. La copie de copie semble assez fiable sur les compilateurs modernes. Je pense que le scepticisme des gens et l’insistance sur le fait que vous devez vérifier votre tableau de prise en charge du compilateur pour RVO sont pour l’essentiel obsolètes de nos jours.


En bref, C++ 11 ne change rien à cet égard, à l'exception des personnes qui ne font pas confiance à la copie.

43
bames53

Presque.

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

L’existence de la sémantique de déplacement a éliminé un cas d’utilisation pour std::string const& - si vous envisagez de stocker le paramètre, prendre un std::string par valeur est plus optimal, car vous pouvez move sur le paramètre.

Si quelqu'un appelle votre fonction avec un C "string" brut, cela signifie qu'un seul tampon std::string est alloué, par opposition à deux dans le cas std::string const&.

Cependant, si vous n'avez pas l'intention de faire une copie, la prise de 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 des tampons de caractères terminés par '\0' de style C, vous pouvez obtenir plus efficacement une fonctionnalité semblable à std::string sans risquer de perdre la mémoire. allocation. Une chaîne C brute peut même être transformée en un std::string_view sans aucune affectation ni copie de caractère.

À ce stade, l’utilisation de std::string const& est lorsque vous ne copiez pas les données en gros et que vous allez les transférer à une API de style C qui attend un tampon à terminaison null et vous avez besoin de la chaîne de niveau supérieur. fonctions que std::string fournit. En pratique, il s'agit d'un ensemble d'exigences rares.

23

std::string n'est pas Plain Old Data (POD) , et sa taille brute n'est pas la chose la plus pertinente de tous les temps. Par exemple, si vous transmettez une chaîne dépassant la longueur de l'authentification unique et allouée sur le tas, je m'attendrais à ce que le constructeur de la copie ne copie pas le stockage de l'authentification unique.

Cela est recommandé parce que inval est construit à partir de l'expression d'argument et est donc toujours déplacé ou copié selon les besoins. Il n'y a aucune perte de performance, en supposant que vous ayez besoin de la propriété de l'argument. Sinon, une référence const pourrait toujours être la meilleure solution.

16
Puppy

J'ai copié/collé la réponse de cette question ici et modifié 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 génère:

$ 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 déménagement:

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

La solution de pass-by-value ne nécessite qu'une seule surcharge mais coûte une construction de mouvement supplémentaire lors du passage de valeurs lvalues ​​et xvalues. Cela peut être ou ne pas être acceptable pour une situation donnée. Les deux solutions présentent des avantages et des inconvénients.

16
Howard Hinnant

Herb Sutter, ainsi que Bjarne Stroustroup, recommandent toujours 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 un littéral de chaîne à un paramètre const std::string&, il fera passer une référence à une chaîne temporaire, créée à la volée pour contenir les caractères de le littéral. Si vous sauvegardez ensuite cette référence, elle ne sera plus valide une fois que la chaîne temporaire aura été désallouée. Pour plus de sécurité, vous devez enregistrer une copie et non la référence. Le problème provient du fait que les littéraux de chaîne sont des types const char[N], nécessitant une promotion vers std::string.

Le code ci-dessous illustre le piège et la solution de contournement, ainsi qu'une option d'efficacité mineure - la surcharge avec une méthode const char*, comme décrit à l'étape Existe-t-il un moyen de passer un littéral de chaîne comme référence en C++ =.

(Remarque: Sutter & Stroustroup vous conseillent de fournir une fonction surchargée avec un paramètre && et un std :: move () si vous conservez une copie de la chaîne.)

#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
}

SORTIE:

const char * constructor
const std::string& constructor

Second string
Second string
13
circlepi314

IMO utilisant la référence C++ pour std::string est une optimisation locale rapide et courte, tandis que le passage par valeur pourrait être (ou non) une meilleure optimisation globale.

Donc, la réponse est: cela dépend des circonstances:

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

Voir "Herb Sutter" Revenir à l'essentiel! L'essentiel du style C++ moderne " . Il passe en revue, entre autres, les conseils de paramétrage donnés dans le passé et les nouvelles idées fournies par C +. +11 et s'intéresse plus particulièrement à l'idée de passer des chaînes de caractères par valeur.

slide 24

Les repères montrent que passer std::strings par valeur, dans les cas où la fonction le copiera de toute façon, peut être considérablement plus lent!

Cela est dû au fait que vous l'obligez à toujours faire une copie complète (puis à la remplacer), tandis que la version const& mettra à jour l'ancienne chaîne, ce qui peut réutiliser le tampon déjà alloué.

Voir sa diapositive 27: Pour les fonctions "définies", l’option 1 est la même qu’elle a toujours été. 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 “récepteur” pour lesquels une chaîne doit être créée (sa valeur existante n'est pas modifiée) que l'astuce passage par valeur est valide. C'est-à-dire, 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 de Nicolai Josuttis et bonne chance avec celle-ci ( “Parfait - Fait!” n fois après avoir trouvé une erreur avec la version précédente. Vous êtes déjà venu là-bas?)


Ceci est également résumé par ⧺F.15 dans les Directives standard.

5
JDługosz

Comme @ JDługosz le souligne dans les commentaires, Herb donne d'autres conseils dans une autre conversation (plus tard?), Voyez à partir de là: https://youtu.be/xnqTKD8uD64?t=54m50s .

Son conseil revient à n'utiliser que des paramètres de valeur pour une fonction f qui prend ce que l'on appelle des arguments récepteurs, en supposant que vous déplacerez une construction à partir de ces arguments récepteurs.

Cette approche générale n’ajoute que la surcharge d’un constructeur de déplacements pour les arguments lvalue et rvalue par rapport à une implémentation optimale de f adaptée aux arguments lvalue et rvalue, respectivement. Pour voir pourquoi c'est le cas, supposons que f prenne un paramètre de valeur, où T représente un type constructible de copie et de déplacement:

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

Si vous appelez f avec un argument lvalue, un constructeur de copie sera appelé pour construire x, et un constructeur de déplacement sera appelé pour construire y. D'autre part, l'appel de f avec un argument rvalue entraînera l'appel d'un constructeur de déplacement pour construire x et d'un autre constructeur de déplacement pour construire y.

En général, l'implémentation optimale de f pour les arguments 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 de f pour les arguments rvalue est, encore une fois, généralement 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 judicieux consiste donc à prendre un paramètre de valeur et à avoir un appel de constructeur de mouvement supplémentaire pour les arguments lvalue ou rvalue en ce qui concerne l'implémentation optimale, ce qui est également le conseil donné dans la présentation de Herb.

Comme @ JDługosz l'a souligné dans les commentaires, passer par valeur n'a de sens que pour les fonctions qui construiront un objet à partir de l'argument sink. Lorsque vous avez une fonction f qui copie son argument, l'approche passage par valeur aura plus de temps système que l'approche générale passage par référence const. L'approche par valeur pour une fonction f qui conserve une copie de son paramètre aura la forme suivante:

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 affectation de construction et 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 seule affectation, ce qui est potentiellement beaucoup moins cher que le constructeur de la copie plus l’affectation de déplacement requise pour l’approche par valeur. La raison en est que l’affectation pourrait réutiliser la mémoire allouée existante dans y et empêcher ainsi les (dés) allocations, alors que le constructeur de copie allouera généralement de la mémoire.

Pour un argument rvalue, l'implémentation la plus optimale pour f qui conserve une copie a la forme suivante:

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

Donc, seule une affectation de déménagement dans ce cas. Passer une valeur rvalue à la version de 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 de f prenant une référence const dans ce cas car l’implémentation générale est préférable.

Donc, en général, pour une mise en oeuvre optimale, vous devrez surcharger ou effectuer une sorte de transfert parfait, comme indiqué dans l’exposé. L'inconvénient est une explosion combinatoire du nombre de surcharges requises, dépendant du nombre de paramètres de f au cas où vous choisiriez de surcharger la catégorie de valeur de l'argument. Le transfert parfait a pour inconvénient que f devient une fonction de modèle, ce qui empêche de le rendre virtuel et génère un code beaucoup plus complexe si vous voulez que tout soit correct à 100% (voir la discussion pour plus de détails).

2
Ton van den Heuvel

Le problème est que "const" est un qualificatif non granulaire. Ce que l'on entend habituellement par "const string ref" est "ne modifiez pas cette chaîne", et non "ne modifiez pas le nombre de références". Il n’est tout simplement pas possible, en C++, de dire qui les membres sont "const". Ils sont tous, ou aucun d'entre eux sont.

Afin de résoudre ce problème de langage, STL pourrait autoriser "C ()" dans votre exemple à créer une copie sémantique avec déplacement de toute façon, et ignorer consciencieusement le "const" en ce qui concerne le compte de référence (mutable). Tant que c'était bien spécifié, ça irait.

Etant donné que STL n’est pas disponible, j’ai une version de chaîne qui contient const_casts <> sans le compteur de références (aucun moyen de rendre rétroactivement quelque chose de mutable dans une hiérarchie de classes), et vous pouvez librement passer des chaînes de caractères à des constantes, et en faire des copies dans des fonctions profondes, toute la journée, sans fuites ni problèmes.

Comme C++ n'offre aucune "granularité constante de classe dérivée", rédiger une bonne spécification et créer un nouvel objet "const movable string" (cmstring) est la meilleure solution que j'ai vue.

1
Erik Aronesty