web-dev-qa-db-fra.com

Dois-je retourner std :: strings?

J'essaie d'utiliser std::string au lieu de char* dans la mesure du possible, mais je crains de trop dégrader les performances. Est-ce un bon moyen de renvoyer des chaînes (pas d'erreur pour vérifier la brièveté)?

std::string linux_settings_provider::get_home_folder() {
    return std::string(getenv("HOME"));
}

En outre, une question connexe: lorsque j'accepte des chaînes comme paramètres, dois-je les recevoir en tant que const std::string& ou const char*?

Merci.

65
Pedro d'Aquino

Renvoie la chaîne.

Je pense que la meilleure abstraction en vaut la peine. Jusqu'à ce que vous puissiez mesurer une différence de performance significative, je dirais que c'est une micro-optimisation qui n'existe que dans votre imagination.

Il a fallu de nombreuses années pour obtenir une bonne abstraction de chaîne en C++. Je ne crois pas que Bjarne Stroustroup, si célèbre pour son dicton conservateur "ne payez que pour ce que vous utilisez", aurait permis un tueur de performance évident dans la langue. Une abstraction plus élevée est bonne.

63
duffymo

Renvoyez la chaîne, comme tout le monde le dit.

lorsque j'accepte des chaînes comme paramètres, dois-je les recevoir sous la forme const std::string& ou const char*?

Je dirais que nous prenons tous les paramètres const par référence, à moins qu'ils ne soient assez légers pour être pris en valeur, ou dans les rares cas où vous avez besoin d'un pointeur nul pour être une entrée valide signifiant "rien de ce qui précède". Cette stratégie n'est pas spécifique aux chaînes.

Les paramètres de référence non const sont discutables, car à partir du code appelant (sans un bon IDE), vous ne pouvez pas immédiatement voir s'ils sont passés par valeur ou par référence, et la différence est importante. Le code peut donc ne pas être clair. Pour les paramètres const, cela ne s'applique pas. Les gens qui lisent le code d'appel peuvent généralement supposer que ce n'est pas leur problème, ils n'auront donc qu'à vérifier la signature à l'occasion.

Dans le cas où vous allez prendre une copie de l'argument dans la fonction, votre politique générale devrait être de prendre l'argument par valeur. Ensuite, vous avez déjà une copie que vous pouvez utiliser, et si vous l'auriez copiée dans un emplacement spécifique (comme un membre de données), vous pouvez la déplacer (en C++ 11) ou l'échanger (en C++ 03) vers y arriver. Cela donne au compilateur la meilleure opportunité d'optimiser les cas où l'appelant passe un objet temporaire.

Pour string en particulier, cela couvre le cas où votre fonction prend un std::string par valeur, et l'appelant spécifie comme expression d'argument un littéral de chaîne ou un char* pointant vers une chaîne terminée par nul. Si vous avez pris un const std::string& et copié dans la fonction, ce qui entraînerait la construction de deux chaînes.

14
Steve Jessop

Le coût de copie des chaînes par valeur varie en fonction de l'implémentation STL avec laquelle vous travaillez:

  • std :: string sous MSVC utilise l'optimisation de chaîne courte, de sorte que les chaînes courtes (<16 caractères iirc) ne nécessitent aucune allocation de mémoire (elles sont stockées dans la chaîne std :: string elle-même), tandis que les plus longues nécessitent une allocation de tas chaque fois que la chaîne est copiée.

  • std :: string sous GCC utilise une implémentation comptée par référence: lors de la construction d'une chaîne std :: à partir d'un caractère *, une allocation de tas est effectuée à chaque fois, mais lors du passage par valeur à une fonction, un comptage de référence est simplement incrémenté, évitant la allocation de mémoire.

En général, vous feriez mieux d'oublier ce qui précède et de renvoyer std :: strings par valeur, à moins que vous ne le fassiez des milliers de fois par seconde.

re: passage de paramètres, gardez à l'esprit qu'il y a un coût à partir de char * -> std :: string, mais pas à partir de std :: string-> char *. En général, cela signifie que vous feriez mieux d'accepter une référence const à une chaîne std ::. Cependant, la meilleure justification pour accepter une const std :: string & comme argument est qu'alors l'appelé n'a pas besoin d'avoir du code supplémentaire pour vérifier vs null.

12
jskinner

Cela semble être une bonne idée.

Si cela ne fait pas partie d'un logiciel en temps réel (comme un jeu) mais d'une application régulière, tout devrait bien se passer.

Rappelez-vous, " l'optimisation prématurée est la racine de tout mal "

10
kostia

C'est la nature humaine de se soucier des performances, en particulier lorsque le langage de programmation prend en charge l'optimisation de bas niveau. Ce que nous ne devons pas oublier en tant que programmeurs, c'est que les performances du programme ne sont qu'une chose parmi tant d'autres que nous pouvons optimiser et admirer. En plus de la vitesse du programme, nous pouvons trouver la beauté dans nos propres performances. Nous pouvons minimiser nos efforts tout en essayant d'obtenir une sortie visuelle maximale et une interactivité de l'interface utilisateur. Pensez-vous que cela pourrait être plus de motivation que de vous soucier des bits et des cycles à long terme ... Alors oui, retournez la chaîne: s. Ils minimisent la taille de votre code et vos efforts et rendent la quantité de travail que vous mettez moins déprimante.

6
AareP

Dans votre cas, l'optimisation de la valeur de retour aura lieu, donc std :: string ne sera pas copié.

5

Méfiez-vous lorsque vous franchissez les limites d'un module.

Ensuite, il est préférable de renvoyer les types primitifs, car les types C++ ne sont pas nécessairement compatibles binaires entre les différentes versions du même compilateur.

4
Hans Malherbe

Je suis d'accord avec les autres affiches, vous devez utiliser de la ficelle.

Mais sachez qu'en fonction de l'agressivité avec laquelle votre compilateur optimise les temporaires, vous aurez probablement une surcharge supplémentaire (en utilisant un tableau dynamique de caractères). (Remarque: La bonne nouvelle est qu'en C++ 0a, l'utilisation judicieuse des références rvalue ne nécessitera pas d'optimisations du compilateur pour acheter de l'efficacité ici - et les programmeurs pourront faire des garanties de performances supplémentaires sur leur code sans compter sur la qualité de le compilateur.)

Dans votre situation, la surcharge supplémentaire vaut-elle la peine d'introduire une gestion manuelle de la mémoire? La plupart des programmeurs raisonnables seraient en désaccord - mais si votre application finit par avoir des problèmes de performances, l'étape suivante serait de profiler votre application - ainsi, si vous introduisez de la complexité, vous ne le faites qu'une fois que vous avez la preuve qu'il est nécessaire de l'améliorer l'efficacité globale.

Quelqu'un a mentionné que l'optimisation de la valeur de retour (RVO) n'est pas pertinente ici - je ne suis pas d'accord.

Le texte standard (C++ 03) sur ceci se lit (12.2):

[Commencer le devis standard]

Des temporaires de type classe sont créés dans différents contextes: lier une rvalue à une référence (8.5.3), renvoyer une rvalue (6.6.3), une conversion qui crée une rvalue (4.1, 5.2.9, 5.2.11, 5.4) , levant une exception (15.1), entrant dans un gestionnaire (15.3), et dans certaines initialisations (8.5). [Remarque: la durée de vie des objets d'exception est décrite en 15.1. ] Même lorsque la création de l'objet temporaire est évitée (12.8), toutes les restrictions sémantiques doivent être respectées comme si l'objet temporaire avait été créé. [Exemple: même si le constructeur de copie n'est pas appelé, toutes les restrictions sémantiques, telles que l'accessibilité (article 11), doivent être satisfaites. ]

 [Exemple: 
 Struct X {
 X (int); 
 X (const X &); 
 ˜X (); 
}; 
 
 X f (X); 
 
 Void g () 
 {
 X a (1) ; 
 X b = f (X (2)); 
 A = f (a); 
} 

Ici, une implémentation peut utiliser un temporaire dans lequel construire X(2) avant de le passer à f() en utilisant le constructeur de copie de X; alternativement, X(2) peut être construit dans l'espace utilisé pour contenir l'argument. De plus, un temporaire peut être utilisé pour conserver le résultat de f(X(2)) avant de le copier dans b à l'aide du constructeur de copie de X; alternativement, le résultat de f () pourrait être construit en b. D'un autre côté, l'expression a = f (a) nécessite un temporaire pour l'argument a ou le résultat de f(a) pour éviter le repliement indésirable de a. ]

[Fin du devis standard]

Essentiellement, le texte ci-dessus dit que vous pouvez éventuellement compter sur RVO dans des situations d'initialisation, mais pas dans des situations d'affectation. La raison en est que lorsque vous initialisez un objet, il est impossible que ce avec quoi vous l'initialisez soit lié à l'objet lui-même (c'est pourquoi vous ne faites jamais d'auto-vérification dans un constructeur de copie), mais lorsque vous le faites une mission, il pourrait.

Il n'y a rien dans votre code, qui interdit intrinsèquement RVO - mais lisez la documentation de votre compilateur pour vous assurer que vous pouvez vraiment vous y fier, si vous en avez vraiment besoin.

3
Faisal Vali

Je suis d'accord avec duffymo. Vous devez d'abord créer une application de travail compréhensible, puis, si besoin est, optimiser les attaques. C'est à ce stade que vous aurez une idée des principaux goulets d'étranglement et pourrez gérer plus efficacement votre temps en créant une application plus rapide.

1
Brian

Je suis d'accord avec @duffymo. N'optimisez pas tant que vous n'avez pas mesuré, cela est vrai pour les micro-optimisations. Et toujours: mesurez avant et après vous avez optimisé, pour voir si vous avez réellement changé les choses en mieux.

1
JesperE

Retournez la chaîne, ce n'est pas une grosse perte en termes de performances, mais cela facilitera sûrement votre travail par la suite.

De plus, vous pouvez toujours intégrer la fonction, mais la plupart des optimiseurs le corrigeront de toute façon.

1
Gab Royer

Si vous passez une chaîne référencée et que vous travaillez sur cette chaîne, vous n'avez rien à renvoyer. ;)

1
Partial