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

J'ai entendu un discours récent de Herb Sutter qui a suggéré que les raisons de passer std::vector et std::string par const & sont en grande partie disparu. 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 à l'endroit où la fonction retourne et peut donc être retourné en utilisant la sémantique de mouvement, qui sont très bon marché. Cependant, inval est encore beaucoup plus grand que le taille d'une référence (qui est habituellement implémentée comme un pointeur). Cela est dû au fait qu'un std::string possède 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 est toujours une bonne idée.

est-ce que quelqu'un peut expliquer pourquoi Herb aurait pu dire cela?

521
demandé sur Mateen Ulhaq 2012-04-19 19:20:57

13 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 ne se soucie pas de C ; tout ce que A sait est B . C'est-à-dire, 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 il ressemble à quelque chose comme 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. Tu ne fais que donner des conseils, 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 utilise simplement.

maintenant, je veux faire un changement simple: C il faut ranger la ficelle quelque part.

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

Bonjour, constructeur de copie et le potentiel d'allocation de mémoire (ignorer le Court Optimisation de la Chaîne d'authentification unique (SSO) ). La sémantique des mouvements de C++11 est censée permettre de supprimer la construction inutile de copies, 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 s'enfuir avec ce qu'on lui a donné.

sauf que c'est impossible. Parce qu'il faut un const& .

si je change C pour prendre son paramètre en valeur, cela fait juste B pour faire la copie dans ce paramètre; Je ne gagne rien.

donc si j'avais juste passé str par valeur à travers toutes les fonctions, en comptant sur std::move pour mélanger les données autour, nous n'aurions pas ce problème. Si quelqu'un veut s'y accrocher, il peut. Si ils ne le faites pas, oh bien.

est-ce Plus cher? Oui, le passage à une valeur est plus coûteux que l'utilisation de références. Est-il moins cher que la copie? Pas pour de petites ficelles avec le SSO. Est-ce la peine de le faire?

cela dépend de votre étui. Combien tu détestes les allocations de mémoire?

354
répondu Nicol Bolas 2017-05-23 12:18:24

les jours de passage de const std::string & comme paramètre sont-ils finis?

Non . De nombreuses personnes prennent ce conseil (y compris Dave Abrahams) au-delà du domaine auquel il s'applique, et le simplifient pour l'appliquer à tous" 1519130920 std::string paramètres -- toujours passer std::string en valeur n'est pas une "meilleure pratique" pour tous les paramètres et applications arbitraires parce que les optimisations ces exposés / articles se concentrent sur Appliquer seulement à un ensemble restreint de cas .

si vous retournez une valeur, modifiez le paramètre, ou prenez la valeur, alors le fait de passer par la valeur pourrait sauver la copie coûteuse et offrir la commodité syntaxique.

comme toujours, passer par const reference sauve beaucoup de copie quand vous n'avez pas besoin d'une copie .

maintenant à l'exemple spécifique:

cependant inval est encore beaucoup plus grand que la taille d'une référence (qui est habituellement implémentée comme un pointeur). Cela est dû au fait qu'une chaîne de caractères std::possède plusieurs composants, dont un pointeur dans le tas et un char[] pour l'optimisation des chaînes courtes. Il me semble donc que le passage par référence est toujours une bonne idée. Quelqu'un peut expliquer pourquoi Herb a pu dire ça?

si la taille de la cheminée est préoccupante (et en supposant ce n'est pas inline/optimisé), return_val + inval > return_val -- OIE, le pic de l'utilisation des piles peut être réduit en passant par valeur ici (remarque: la simplification excessive de ABIs). Pendant ce temps, passer par la référence const peut désactiver les optimisations. La raison principale ici n'est pas d'éviter la croissance de la cheminée, mais de s'assurer que l'optimisation peut être effectuée où il est applicable .

Les jours de passage par référence const les règles sont plus compliquées qu'elles ne l'étaient. Si la performance est importante, vous serez avisé de considérer comment vous passer ces types, en fonction des détails que vous utilisez dans vos implémentations.

127
répondu justin 2018-01-17 21:28:12

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

Toutefois, 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 mises en œuvre dans une unité de compilation séparée afin d'éviter l'inclinaison. Puis:

1. Si vous passez un littéral de ces deux fonctions, vous ne verrez pas beaucoup de différence dans les performances. Dans les deux cas, un objet string a à créer

2. Si vous passez un autre objet std::string, foo2 sera supérieur à foo1 , parce que foo1 fera une copie profonde.

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 en 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
répondu BЈовић 2012-04-19 16:05:05

sauf si vous avez réellement besoin d'une copie, il est tout de même raisonnable de prendre const & . Par exemple:

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

si vous changez ceci pour prendre la chaîne en valeur, alors vous finirez par déplacer ou copier le paramètre, et il n'y a pas besoin de cela. Non seulement la copie/le déplacement est probablement plus coûteux, mais il introduit également un nouvel échec potentiel; la copie / le déplacement pourrait jeter une exception (par exemple, l'attribution pendant la copie pourrait échouer) tandis que prendre une référence à une valeur existante Je ne peux pas.

Si vous faire besoin d'une copie puis passage et le retour par valeur est généralement (toujours?) la meilleure option. En fait, je ne m'en soucierais généralement pas en C++03 à moins que vous ne trouviez que des copies supplémentaires causent un problème de performance. Copie élision 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 soutien de compilateur pour RVO est la plupart du temps obsolète de nos jours.


bref, C++11 ne change rien à cet égard sauf pour les gens qui ne se fiaient pas à la copie.

41
répondu bames53 2012-04-19 15:50:31

courte réponse: Non! longue réponse:

  • Si vous ne voulez pas modifier la chaîne (traiter est en lecture seule), passer comme const ref& .

    (le const ref& doit évidemment rester dans la portée tandis que la fonction qui l'utilise exécute)
  • si vous prévoyez de le modifier ou si vous savez qu'il sortira du champ d'application (threads) , passez-le comme un value , ne copiez pas le const ref& à l'intérieur de votre corps de fonction.

Il y a un post sur cpp-next.com appelé "Veulent la vitesse, le passage par valeur!" . Le TL; DR:

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

traduction de

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

, ne faites pas ceci :

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

faire :

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

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

vous avez juste besoin d'être conscient comment vous prévoyez d'utiliser l'argument dans le corps de fonction. En lecture seule ou non... et si ça reste dans la portée.

41
répondu CodeAngry 2015-08-27 13:59:05

presque.

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

L'existence de la sémantique des mouvements a éliminé un cas d'utilisation pour std::string const& -- si vous prévoyez de stocker le paramètre, prendre un std::string par valeur est plus optimal, comme vous pouvez move hors du paramètre.

si quelqu'un a appelé votre fonction avec un c brut "string" cela signifie qu'un seul tampon std::string est jamais alloué, par opposition à deux dans le cas std::string const& .

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

avec std::string_view , tant que vous ne passez pas ladite chaîne à une API qui attend des tampons de caractères de style C '"1519100920"' , vous pouvez plus efficacement obtenir std::string comme fonctionnalité sans risquer allocation. Une chaîne en C brute peut même être transformée en std::string_view sans aucune attribution ou copie de caractère.

à ce moment, l'utilisation pour std::string const& est quand vous ne copiez pas les données en gros, et allez les transmettre à une API de style C qui attend un tampon terminé null, et vous avez besoin des fonctions de chaîne de niveau supérieur que std::string fournit. Dans la pratique, il s'agit d'un ensemble rare d'exigences.

19
répondu Yakk - Adam Nevraumont 2018-06-11 09:30:03

std::string n'est pas Plaine des Données Anciennes(POD) , et ses brutes de taille n'est pas le plus pertinent chose jamais. Par exemple, si vous passez dans une chaîne qui est au-dessus de la longueur de SSO et affectée sur le tas, Je m'attendrais à ce que le constructeur de copie ne copie pas le stockage SSO.

la raison pour laquelle ceci est recommandé est parce que inval est construit à partir de l'expression argument, et est donc toujours déplacé ou copié comme approprié - il n'y a pas perte de performance, en supposant que vous avez besoin de propriété de l'argument. Si vous ne le faites pas, une référence const pourrait être la meilleure façon de procéder.

16
répondu Puppy 2017-05-23 11:55:03

j'ai copié / collé la réponse de cette question ici, et j'ai changé les noms et l'orthographe pour correspondre à 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, c'sorties:

$ 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 numéro de la copie des constructions et le deuxième nombre est le nombre de déplacer constructions:

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

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

15
répondu Howard Hinnant 2017-05-23 12:02:48

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 non mentionné dans aucune des autres réponses ici: si vous passez une chaîne littérale à un paramètre const std::string& , 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 cette référence, elle sera valable une fois que la chaîne temporaire est libéré. Pour être sûr , vous devez enregistrer un copie , pas la référence. Le problème vient du fait que les caractères littéraux des cordes sont de type const char[N] , ce qui nécessite une promotion à std::string .

le code ci-dessous illustre la chute et la solution, ainsi qu'une option d'efficacité mineure -- surcharge avec une méthode const char* , comme décrit à est-il possible de passer une chaîne de caractères littérale comme référence en C++ .

(Note: Sutter & Stroustroup conseillent que si vous conservez une copie de la chaîne, fournir également une fonction surchargée avec a && parameter et std::move () it.)

#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
répondu circlepi314 2017-05-23 11:47:32
fortement code de bibliothèque où les chaînes sont passées, vous gagnerez probablement plus dans le sens global en faisant confiance au comportement du constructeur de copie std::string .
7
répondu digital_infinity 2012-04-25 12:03:06

Voir "Herb Sutter "Retour à l'essentiel! L'essentiel de C++ Moderne de Style" . Entre autres sujets, il passe en revue les conseils de passage de paramètre qui ont été donnés dans le passé, et de nouvelles idées qui viennent avec C++11 et se penche spécifiquement sur l'idée de passer des cordes par valeur.

slide 24

les benchmarks montrent que le passage 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 (et ensuite passer à la place), tandis que la version const& 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 qu'elle l'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.

c'est seulement pour les paramètres" sink " où une chaîne doit être créée (sa valeur existante ne doit pas être changée) que le truc du pass-by-value est valide. C'est-à-dire constructors dans lequel le paramètre initialise directement le membre du type correspondant.

si vous voulez voir à quel point vous pouvez aller dans s'inquiéter à ce sujet, regarder Nicolai Josuttis présentation et bonne chance avec cela ( "parfait - fait!" n fois après avoir trouvé un défaut avec la version précédente. Jamais été là?)


C'est aussi désigné comme ⧺F. 15 dans la Norme de lignes Directrices.

3
répondu JDługosz 2018-05-09 01:25:53

comme le souligne @JDługosz dans les commentaires, Herb donne d'autres conseils dans un autre (plus tard?) de parler, de voir à peu près à partir d'ici: https://youtu.be/xnqTKD8uD64?t=54m50s .

ses conseils se résument à n'utiliser que des paramètres de valeur pour une fonction f qui prend les soi-disant arguments de sink, en supposant que vous allez déplacer construire à partir de ces arguments de sink.

cette approche générale ne fait qu'ajouter les frais généraux d'un déménagement constructeur pour les arguments lvalue et rvalue par rapport à une implémentation optimale de f adaptée respectivement aux arguments lvalue et rvalue. Pour voir pourquoi c'est le cas, supposons que f prend un paramètre de valeur, où T est un type de copie et de déplacement constructible:

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

appeler f avec un argument de valeur l se traduira par un constructeur de copie étant appelé à construire x , et un constructeur de déplacement étant appelé pour construire y . D'un autre côté , appeler f avec un argument rvalue fera qu'un constructeur de déplacement sera appelé à construire x , et un autre constructeur de déplacement sera appelé à construire y .

en général, la mise en œuvre optimale de f pour les arguments de valeur est la suivante:

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

dans ce cas, un seul constructeur de copies est appelé à construire y . Optimal la mise en œuvre de f pour les arguments de valeur est, encore une fois en général, comme suit:

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

dans ce cas, un seul constructeur de mouvements est appelé à construire y .

donc un compromis raisonnable est de prendre un paramètre de valeur et d'avoir un appel de constructeur de mouvement supplémentaire pour les arguments lvalue ou rvalue en ce qui concerne l'implémentation optimale, qui est également le conseil donné dans Herb's talk.

As @JDługosz a souligné dans les commentaires, le fait de passer par la valeur n'a de sens que pour les fonctions qui construiront un objet à partir de l'argument de l'évier. Lorsque vous avez une fonction f qui copie son argument, l'approche pass-by-value aura plus de frais généraux qu'une approche générale pass-by-const-reference. L'approche pass-by-value pour une fonction f qui conserve une copie de son paramètre aura la forme:

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

dans ce cas, il y a une copie construction et une assignation de déménagement pour un argument de valeur l, et une assignation de construction et de déménagement pour un argument de valeur R. Le cas le plus optimal pour un argument de valeur l est:

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

cela se résume à une affectation seulement, ce qui est potentiellement beaucoup moins coûteux que la copie constructeur plus l'affectation de déplacement requise pour l'approche pass-by-value. La raison en est que la cession pourrait réutiliser la mémoire allouée existante dans y , et donc prévient (de)les allocations, alors que le constructeur de copie alloue habituellement la mémoire.

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

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

donc, seulement une affectation de déménagement dans ce cas. Le fait de passer une valeur de R à la version de f qui prend une référence de const ne coûte qu'une affectation au lieu d'une affectation de déménagement. Donc relativement parlant, la version de f prise const référence dans ce cas, comme la mise en œuvre générale est préférable.

donc, en général, pour la mise en œuvre la plus optimale, vous aurez besoin de surcharger ou de faire une sorte de transit parfait comme indiqué dans la présentation. L'inconvénient est une explosion combinatoire du nombre de surcharges nécessaire, en fonction du nombre de paramètres pour f dans le cas où vous optez pour la surcharge sur la valeur de l'argument. Perfect forwarding a l'inconvénient que f devient une fonction de modèle, ce qui empêche de le rendre virtuel, et se traduit par un code beaucoup plus complexe si vous voulez l'obtenir à 100% (voir la discussion pour les détails sanglants).

2
répondu Ton van den Heuvel 2018-06-27 13:48:56

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", pas "Ne modifiez pas le nombre de référence". Il n'y a tout simplement pas moyen, en C++, de dire que les membres sont "const". Soit ils le sont tous, ou aucun d'entre eux le sont.

afin de hacker cette question de langue, STL pourrait permettre" C () " dans votre exemple pour faire un mouvement-copie sémantique de toute façon , et ignorer consciemment le" const " en ce qui concerne le nombre de référence (mutable). Tant qu'il était bien spécifié, ce serait parfait.

Depuis STL n'est pas le cas, j'ai une version d'une chaîne qui const_casts<> loin le compteur de référence (aucun moyen de rétroactivement faire quelque chose mutable dans une hiérarchie de classe), et - et voilà - vous pouvez passer librement cmstring est comme const références, et en prendre copie en profondeur des fonctions, tout au long de la journée, avec pas de fuites ou de questions.

puisque C++ n'offre pas de" granularité de const dérivée " ici, écrire une bonne spécification et faire un nouvel objet brillant "const movable string" (cmstring) est la meilleure solution que j'ai vu.

1
répondu Erik Aronesty 2018-05-10 20:53:28