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

Herb Sutter m’a récemment parlé des raisons pour lesquelles std::vector et std::ssortingng by const & disparu. Il a suggéré que l’écriture d’une fonction telle que la suivante est maintenant préférable:

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

Je comprends que return_val sera une valeur au point 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, inval est toujours beaucoup plus grande 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 que std::ssortingng 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?

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 C A ne sait pas ou se soucie de C ; tout ce que A sait 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::ssortingng &str) { C(str); } void C(const std::ssortingng &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& 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::ssortingng &str) { //Do something with `str`. m_str = str; } 

Bonjour, copiez le constructeur et l’allocation de mémoire potentielle (ignorez le SSO (Short Ssortingng Optimization) ). 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 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 pour prendre son paramètre par valeur, cela fait que B fait la copie dans ce paramètre; Je ne gagne rien.

Donc, si je venais de passer str par valeur à toutes les fonctions, en me basant sur std::move pour std::move les données, nous n’aurions pas ce problème. Si quelqu’un veut le garder, il le peut. S’ils ne le font pas, eh bien.

Est-ce plus cher? Oui; le passage à une valeur est plus coûteux que l’utilisation de références. Est-ce moins cher que la copie? Pas pour les petites chaînes avec SSO. Est-ce la peine de faire?

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

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

Non Beaucoup de gens prennent ce conseil (y compris Dave Abrahams) au-delà du domaine auquel il s’applique, et le simplifient pour qu’il s’applique à tous std::ssortingng parameters std::ssortingngToujours passer std::ssortingng par valeur n’est pas une “pratique exemplaire” pour tout parameters et applications arbitraires car les optimisations sur lesquelles ces discussions / articles se concentrent ne s’appliquent qu’à un ensemble restreint de cas .

Si vous retournez une valeur, modifiez le paramètre ou prenez la valeur, le passage à une valeur peut permettre d’économiser des copies coûteuses et offrir une commodité syntaxique.

Comme toujours, le passage par la référence const permet d’économiser beaucoup de copies lorsque vous n’avez pas besoin de 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). Cela est dû au fait que std :: ssortingng 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 stack est un problème (et en supposant que ce n’est pas intégré / optimisé), return_val + inval > return_val – IOW, l’utilisation maximale de la stack peut être réduite en faisant passer la valeur ici (remarque: simplification excessive des ABI). Pendant ce temps, le passage par référence de const peut désactiver les optimisations. La principale raison ici n’est pas d’éviter la croissance de la stack, mais de s’assurer que l’optimisation peut être effectuée là où elle est applicable .

Les jours de passage par la référence const ne sont pas révolus, les règles étant plus compliquées qu’elles ne l’étaient auparavant. Si les performances sont importantes, vous devrez réfléchir à la manière dont vous transmettez ces types, en fonction des détails que vous utilisez dans vos implémentations.

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

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

Permet de considérer les fonctions suivantes:

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

Ces fonctions sont implémentées dans une unité de compilation séparée afin d’éviter l’incrustation. Alors :
1. Si vous transmettez un littéral à ces deux fonctions, vous ne verrez pas beaucoup de différence dans les performances. Dans les deux cas, un object chaîne doit être créé
2. Si vous passez un autre object std :: ssortingng, foo2 surperformera foo1 , car foo1 effectuera 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 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

À moins que vous ayez réellement besoin d’une copie, il est toujours raisonnable de prendre const & . Par exemple:

 bool isprint(std::ssortingng 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, ce qui n’est pas nécessaire. Non seulement le copier / déplacer est-il plus coûteux, mais il introduit également une nouvelle défaillance potentielle; Le copier / déplacer peut générer une exception (par exemple, une allocation pendant la copie peut échouer) alors que prendre une référence à une valeur existante ne peut pas.

Si vous avez besoin d’une copie, alors passer et renvoyer par 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 à moins que vous trouviez que des copies supplémentaires causent réellement un problème de performance. L’élision de copie semble assez fiable sur les compilateurs modernes. Je pense que le scepticisme des gens et leur insistance à vérifier la compatibilité de votre table de compilation avec RVO sont pour la plupart obsolètes de nos jours.


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

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

  • Si vous ne modifiez pas la chaîne (traiter est en lecture seule), transmettez-la en tant que const ref& .
    (le const ref& doit évidemment restr dans la scope pendant que la fonction qui l’utilise s’exécute)
  • Si vous prévoyez de le modifier ou que vous savez qu’il sortira de la scope (threads) , transmettez-le en tant que value , ne copiez pas la const ref& intérieur de votre corps de fonction.

Il y avait un post sur cpp-next.com intitulé “Vitesse, passe par valeur!” . Le TL; DR:

Ligne direcsortingce : ne copiez pas vos arguments de fonction. Au lieu de cela, transmettez-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 le copiant dans une variable interne, utilisez plutôt un argument value .

Alors, ne fais pas ça :

 std::ssortingng function(const std::ssortingng& aSsortingng){ auto vSsortingng(aSsortingng); vSsortingng.clear(); return vSsortingng; } 

fais ceci :

 std::ssortingng function(std::ssortingng aSsortingng){ aSsortingng.clear(); return aSsortingng; } 

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

Vous devez simplement savoir comment vous prévoyez d’utiliser l’argument dans le corps de la fonction. Lecture seule ou NON … et si elle rest dans la scope.

Presque.

En C ++ 17, nous avons basic_ssortingng_view , Ce qui nous ramène à un cas d’utilisation étroit pour std::ssortingng const& parameters.

L’existence de la sémantique de déplacement a éliminé un cas d’utilisation de std::ssortingng const& si vous prévoyez de stocker le paramètre, il est std::ssortingng prendre un std::ssortingng par valeur, car vous pouvez sortir du paramètre.

Si quelqu’un a appelé votre fonction avec une "ssortingng" C brute, cela signifie qu’un seul tampon std::ssortingng est jamais alloué, au lieu de deux dans le std::ssortingng const& case.

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

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

À ce stade, l’utilisation de std::ssortingng const& se produit lorsque vous ne copiez pas la vente en gros de données et que vous la transmettez à une API de style C qui attend un tampon terminé par null et que vous avez besoin de la chaîne de niveau supérieur fonctions que std::ssortingng fournit. En pratique, il s’agit d’un ensemble rare d’exigences.

std::ssortingng n’est pas Plain Old Data (POD) et sa taille brute n’est jamais la plus pertinente. Par exemple, si vous transmettez une chaîne supérieure à la longueur de la connexion unique 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 argument, et est donc toujours déplacé ou copié comme il convient. Si vous ne le faites pas, une référence const pourrait toujours être la meilleure solution.

J’ai copié / collé la réponse de cette question ici, et changé les noms et l’orthographe pour répondre à cette question.

Voici le code pour mesurer ce qui est demandé:

 #include  struct ssortingng { ssortingng() {} ssortingng(const ssortingng&) {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: ssortingng(const ssortingng&) ssortingng(ssortingng&&) do_something with xvalue: ssortingng(ssortingng&&) ssortingng(ssortingng&&) do_something with prvalue: ssortingng(ssortingng&&) $ clang++ -std=c++11 -stdlib=libc++ -DPROCESS=2 test.cpp $ a.out do_something with lvalue: ssortingng(const ssortingng&) do_something with xvalue: ssortingng(ssortingng&&) do_something with prvalue: ssortingng(ssortingng&&) 

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 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 présentent des avantages et des inconvénients.

Herb Sutter est toujours enregistré, avec Bjarne Stroustroup, en recommandant const std::ssortingng& 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 un littéral de chaîne à un const std::ssortingng& parameter, il passera une référence à une chaîne temporaire, créée à la volée pour contenir les caractères de le littéral. Si vous enregistrez ensuite cette référence, elle sera invalide une fois la chaîne temporaire libérée. Pour être sûr, vous devez enregistrer une copie , pas 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 en std::ssortingng .

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 à la section Existe-t-il un moyen de transmettre une chaîne littérale comme référence en C ++ .

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

 #include  #include  class WidgetBadRef { public: WidgetBadRef(const std::ssortingng& s) : myStrRef(s) // copy the reference... {} const std::ssortingng& myStrRef; // might be a reference to a temporary (oops!) }; class WidgetSafeCopy { public: WidgetSafeCopy(const std::ssortingng& s) : myStrCopy(s) // constructor for ssortingng references; copy the ssortingng {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::ssortingng& constructor Second ssortingng Second ssortingng 

IMO utilisant la référence C ++ pour std::ssortingng est une optimisation locale rapide et courte, alors 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 les fonctions internes, vous savez ce que le code fait, vous pouvez utiliser la référence const std::ssortingng & .
  2. Si vous écrivez le code de la bibliothèque ou utilisez beaucoup le code de la bibliothèque où les chaînes sont passées, vous gagnez probablement plus en sens global en faisant confiance au comportement du constructeur de copie std::ssortingng .

Voir “Herb Sutter”, l’essentiel du style C ++ moderne . Il passe en revue, entre autres, les conseils sur le passage des parameters et les nouvelles idées qui accompagnent C ++ 11. idée de passer des chaînes par valeur.

diapositive 24

Les tests montrent que le passage de std::ssortingng s par valeur, dans les cas où la fonction le copiera de toute façon, peut être nettement plus lent!

C’est parce que vous le forcez à toujours faire une copie complète (puis à se mettre en place), tandis que la version const& version ancienne met à 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 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 parameters.

Ce n’est que pour les parameters «puits» où une chaîne doit être créée (dont la valeur existante n’a pas été modifiée) que le tour par valeur est valide. C’est-à-dire des constructeurs dans lesquels le paramètre initialise directement le membre du type correspondant.

Si vous voulez voir à quel point vous pouvez vous soucier de cela, regardez la présentation de Nicolai Josuttis et bonne chance avec ça ( «Perfect – Done!» N fois après avoir trouvé la version précédente).


Ceci est également résumé en ⧺F.15 dans les directives standard.

Comme @ JDługosz le fait remarquer dans les commentaires, Herb donne d’autres conseils dans un autre (plus tard?) Exposé, voir à peu près ici: https://youtu.be/xnqTKD8uD64?t=54m50s .

Son conseil se résume à utiliser uniquement des parameters de valeur pour une fonction f qui prend ce qu’on appelle des arguments de récepteur, en supposant que vous déplacerez la construction de ces arguments de récepteur.

Cette approche générale ajoute uniquement la surcharge d’un constructeur de déplacement 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 prenne un paramètre de valeur, où T est du type constructible copy and move:

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

L’appel de f avec un argument lvalue entraînera l’appel d’un constructeur de copie pour construire x , et un constructeur de déplacement appelé y pour construire. D’un autre côté, l’appel de f avec un argument rvalue provoquera l’appel d’un constructeur de déplacement pour construire x , et 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é à construire y .

Un compromis judicieux consiste donc à prendre un paramètre value et à faire appel à un constructeur de déplacement 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, le passage par value n’a de sens que pour les fonctions qui construiront un object à partir de l’argument de récepteur. Lorsque vous avez une fonction f qui copie son argument, l’approche par valeur aura une surcharge par rapport à une approche générale par référence de constance. 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, et une construction de déplacement et une affectation de déplacement pour un argument rvalue. Le cas le plus optimal pour un argument de lvalue est le suivant:

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

Cela se résume à une tâche uniquement, qui est potentiellement beaucoup moins chère que le constructeur de copie et l’affectation de mouvement requirejse pour l’approche par valeur. La raison en est que l’affectation peut réutiliser la mémoire allouée existante dans y , et donc empêcher les allocations (dé), alors que le constructeur de copie alloue généralement de la mémoire.

Pour un argument rvalue, l’implémentation la plus optimale pour f 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 à 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, comme implémentation générale, est préférable.

Donc, en général, pour une implémentation optimale, vous devez surcharger ou effectuer une sorte de transfert parfait, comme indiqué dans la conférence. L’inconvénient est une explosion combinatoire du nombre de surcharges nécessaires, en fonction du nombre de parameters pour f si vous choisissez de surcharger la catégorie de valeur de l’argument. Le transfert parfait présente l’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 le rendre 100% correct (voir la discussion des détails évidents).

The problem is that “const” is a non-granular qualifier. What is usually meant by “const ssortingng ref” is “don’t modify this ssortingng”, not “don’t modify the reference count”. There is simply no way, in C++, to say which members are “const”. They either all are, or none of them are.

In order to hack around this language issue, STL could allow “C()” in your example to make a move-semantic copy anyway , and dutifully ignore the “const” with regard to the reference count (mutable). As long as it was well-specified, this would be fine.

Since STL doesn’t, I have a version of a ssortingng that const_casts<> away the reference counter (no way to retroactively make something mutable in a class hierarchy), and – lo and behold – you can freely pass cmstring’s as const references, and make copies of them in deep functions, all day long, with no leaks or issues.

Since C++ offers no “derived class const granularity” here, writing up a good specification and making a shiny new “const movable ssortingng” (cmssortingng) object is the best solution I’ve seen.