Est-ce une mauvaise pratique d'écrire du code qui repose sur des optimisations du compilateur?


99

J'ai appris le C ++ et dois souvent renvoyer des objets volumineux à partir de fonctions créées dans la fonction. Je sais qu'il y a passe par référence, renvoyer un pointeur et des solutions de type référence, mais j'ai aussi lu que les compilateurs C ++ (et la norme C ++) permettent l'optimisation des valeurs de retour, ce qui évite de copier ces gros objets en mémoire. économiser le temps et la mémoire de tout cela.

Maintenant, j'estime que la syntaxe est beaucoup plus claire lorsque l'objet est explicitement renvoyé par valeur, et que le compilateur emploiera généralement le RVO et rendra le processus plus efficace. Est-ce une mauvaise pratique de s'appuyer sur cette optimisation? Cela rend le code plus clair et plus lisible par l'utilisateur, ce qui est extrêmement important, mais devrais-je me garder de supposer que le compilateur saisira l'occasion RVO?

S'agit-il d'une micro-optimisation ou d'une chose à garder à l'esprit lors de la conception de mon code?


7
POUR répondre à votre modification, il s’agit d’une micro-optimisation, car même si vous tentiez de comparer ce que vous gagnez en nanosecondes, vous le verriez à peine. Pour le reste, je suis trop pourri en C ++ pour vous donner une réponse stricte sur les raisons pour lesquelles cela ne fonctionnerait pas. L'un d'eux, s'il est probable qu'il existe des cas où vous avez besoin d'une allocation dynamique et utilisez donc nouveau / pointeur / références.
Walfrat

4
@Walfrat même si les objets sont assez volumineux, de l'ordre du mégaoctet? Mes tableaux peuvent devenir énormes à cause de la nature des problèmes que je résous.
Matt

6
@ Matt je ne voudrais pas. Les références / pointeurs existent précisément pour cela. Les optimisations du compilateur sont supposées aller au-delà de ce que les programmeurs devraient prendre en compte lors de la construction d'un programme, même si, oui, les deux mondes se chevauchent souvent.
Neil

5
@Matt À moins que vous ne fassiez quelque chose d'extrêmement spécifique qui suppose de demander aux développeurs ayant une expérience de plus de 10 ans dans les noyaux C / C, une interaction matérielle réduite ne devrait pas être nécessaire Si vous pensez appartenir à quelque chose de très spécifique, éditez votre message et ajoutez une description précise de ce que vous êtes censé faire avec votre application (calcul en temps réel? Calculs lourds? ...)
Walfrat

37
Dans le cas particulier de RVO (C) (C), oui, cette optimisation est parfaitement valable. Cela est dû au fait que la norme C ++ 17 l' exige spécifiquement , dans les situations où les compilateurs modernes le faisaient déjà.
Caleth

Réponses:


130

Employer le principe de moindre surprise .

Est-ce vous et seulement jamais qui utiliserez ce code, et êtes-vous sûr que vous ne serez pas surpris de ce que vous faites dans 3 ans?

Alors vas-y.

Dans tous les autres cas, utilisez la méthode standard. sinon, vous et vos collègues allez rencontrer des difficultés pour trouver des bogues.

Par exemple, mon collègue se plaignait que mon code causait des erreurs. Il s'est avéré qu'il avait désactivé l'évaluation booléenne de court-circuit dans les paramètres de son compilateur. Je l'ai presque giflé.


88
@ Neil, c'est ce que je veux dire, tout le monde compte sur l'évaluation des courts-circuits. Et vous ne devriez pas avoir à y réfléchir à deux fois, il devrait être allumé. C'est un standard de facto. Oui, vous pouvez le changer, mais vous ne devriez pas.
Pieter B

49
"J'ai changé la façon dont la langue fonctionne, et ton code pourri sale a éclaté! Arghh!" Sensationnel. Gifler serait approprié, envoyez votre collègue suivre une formation Zen, il y en a beaucoup.

109
@PieterB Je suis presque sûr que les spécifications des langages C et C ++ garantissent une évaluation des courts-circuits. Ce n'est donc pas simplement une norme de facto, c'est la norme. Sans cela, vous n'utilisez même plus le C / C ++, mais quelque chose qui lui ressemble
étrangement

47
Juste pour référence, la méthode standard ici est de retourner par valeur.
DeadMG

28
@ dan04 oui, c'était à Delphes. Les gars, ne vous laissez pas prendre dans l'exemple, c'est à propos de ce que j'ai dit. Ne faites pas de choses surprenantes que personne ne fait.
Pieter B

81

Pour ce cas particulier, il faut absolument revenir par valeur.

  • RVO et NRVO sont des optimisations bien connues et robustes qui devraient vraiment être faites par tout compilateur décent, même en mode C ++ 03.

  • La sémantique de déplacement garantit que les objets sont déplacés des fonctions si (N) RVO n'a pas eu lieu. Ce n'est utile que si votre objet utilise des données dynamiques en interne (comme le std::vectorfait), mais cela devrait vraiment être le cas si elles sont aussi volumineuses: déborder de la pile est un risque pour les gros objets automatiques.

  • C ++ 17 applique RVO. Alors ne vous inquiétez pas, il ne disparaîtra pas sur vous et ne finira par s'établir complètement que lorsque les compilateurs seront à jour.

Et au final, forcer une allocation dynamique supplémentaire à renvoyer un pointeur, ou forcer votre type de résultat à être constructible par défaut afin que vous puissiez le passer comme paramètre de sortie sont à la fois des solutions laides et non idiomatiques à un problème que vous ne rencontrerez probablement jamais. avoir.

Il suffit d’écrire du code qui a du sens et de remercier les rédacteurs du compilateur d’optimiser correctement le code qui a du sens.


9
Juste pour le plaisir, voyez comment Borland Turbo C ++ 3.0 à partir de 1990 gère RVO . Spoiler: Cela fonctionne fondamentalement très bien.
nwp

9
La clé ici est qu’il ne s’agit pas d’une optimisation aléatoire spécifique au compilateur ou de "fonctionnalité non documentée", mais de quelque chose qui, bien que techniquement optionnel dans plusieurs versions de la norme C ++, a été fortement encouragé par le secteur et que pratiquement tous les principaux compilateurs l’ont fait depuis. un très long temps.

7
Cette optimisation n'est pas aussi robuste qu'on pourrait le souhaiter. Oui, il est assez fiable dans les cas les plus évidents, mais en cherchant par exemple le bugzilla de gcc, il existe de nombreux cas à peine moins évidents où il est omis.
Marc Glisse

62

Maintenant, j'estime que la syntaxe est beaucoup plus claire lorsque l'objet est explicitement renvoyé par valeur, et que le compilateur emploiera généralement le RVO et rendra le processus plus efficace. Est-ce une mauvaise pratique de s'appuyer sur cette optimisation? Cela rend le code plus clair et plus lisible par l'utilisateur, ce qui est extrêmement important, mais devrais-je me garder de supposer que le compilateur saisira l'occasion RVO?

Ce n’est pas une micro-optimisation peu connue, mignonne, dont vous parlez dans un petit blog, qui fait l’objet d’un trafic, puis vous vous sentez malin et supérieur.

Après C ++ 11, RVO est la méthode standard pour écrire ce code de code. Il est courant, attendu, enseigné, mentionné dans les discussions, mentionné dans les blogs, mentionné dans la norme, sera signalé comme un bug du compilateur s'il n'est pas implémenté. En C ++ 17, le langage va encore plus loin et impose la résolution de copie dans certains scénarios.

Vous devez absolument compter sur cette optimisation.

En plus de cela, le retour par valeur conduit simplement à un code extrêmement facile à lire et à gérer qu'un code qui retourne par référence. La sémantique de valeur est une chose puissante, qui pourrait conduire à plus d'opportunités d'optimisation.


3
Merci, cela fait beaucoup de sens et est compatible avec le "principe de moindre surprise" mentionné ci-dessus. Cela rendrait le code très clair et compréhensible, et rendrait plus difficile la confusion avec les manigances.
Matt

3
@Matt Une partie de la raison pour laquelle j'ai voté pour cette réponse est qu'elle mentionne la "sémantique de valeur". Au fur et à mesure que vous acquerrez de l'expérience en C ++ (et en programmation en général), vous rencontrerez parfois des situations dans lesquelles la sémantique des valeurs ne peut pas être utilisée pour certains objets car ils sont mutables et que leurs modifications doivent être rendues visibles pour les autres codes utilisant ce même objet exemple de "mutabilité partagée"). Lorsque ces situations se produisent, les objets affectés doivent être partagés via des pointeurs (intelligents).
Rwong

16

L'exactitude du code que vous écrivez ne doit jamais dépendre d'une optimisation. Il devrait générer le résultat correct une fois exécuté sur la "machine virtuelle" C ++ utilisée dans la spécification.

Cependant, ce dont vous parlez est davantage une question d’efficacité. Votre code fonctionne mieux s'il est optimisé avec un compilateur optimisant RVO. C'est bien, pour toutes les raisons mentionnées dans les autres réponses.

Toutefois, si vous avez besoin de cette optimisation (par exemple, si le constructeur de la copie entraîne l’échec de votre code), vous êtes maintenant à la merci du compilateur.

Je pense que le meilleur exemple de cela dans ma propre pratique est l'optimisation des appels de queue:

   int sillyAdd(int a, int b)
   {
      if (b == 0)
          return a;
      return sillyAdd(a + 1, b - 1);
   }

C'est un exemple stupide, mais il montre un appel final, dans lequel une fonction est appelée récursivement juste à la fin d'une fonction. La machine virtuelle C ++ montrera que ce code fonctionne correctement, bien que je puisse créer un peu de confusion sur les raisons pour lesquelles je me suis ennuyé d'écrire une telle routine d'addition en premier lieu. Cependant, dans les implémentations pratiques de C ++, nous avons une pile et son espace est limité. Si elle est effectuée de manière pédagogique, cette fonction devra au moins b + 1insérer des cadres de pile dans la pile lors de son addition. Si je veux calculer sillyAdd(5, 7), ce n'est pas grave. Si je veux calculer sillyAdd(0, 1000000000), je pourrais avoir vraiment du mal à provoquer un StackOverflow (et non le bon genre ).

Cependant, nous pouvons voir que lorsque nous atteignons la dernière ligne de retour, nous en avons vraiment fini avec tout dans le cadre de pile actuel. Nous n'avons pas vraiment besoin de le garder. L'optimisation des appels en queue vous permet de "réutiliser" le cadre de pile existant pour la fonction suivante. De cette façon, nous n’avons besoin que d’un seul cadre de pile, plutôt que b+1. (Nous devons encore faire toutes ces additions et soustractions idiotes, mais elles ne prennent pas plus de place.) En réalité, l'optimisation convertit le code en:

   int sillyAdd(int a, int b)
   {
      begin:
      if (b == 0)
          return a;
      // return sillyAdd(a + 1, b - 1);
      a = a + 1;
      b = b - 1;
      goto begin;  
   }

Dans certaines langues, l’optimisation de l’appel final est explicitement requise par la spécification. C ++ n'en fait pas partie. Je ne peux pas compter sur les compilateurs C ++ pour reconnaître cette opportunité d'optimisation des appels en aval, sauf si j'y vais au cas par cas. Avec ma version de Visual Studio, la version finale optimise les appels en aval, contrairement à la version de débogage (de par sa conception).

Ainsi , il serait mauvais pour moi dépends d'être en mesure de calculer sillyAdd(0, 1000000000).


2
C'est un cas intéressant, mais je ne pense pas que vous puissiez généraliser à la règle du premier paragraphe. Supposons que j'ai un programme pour un petit périphérique, qui se chargera si et seulement si j'utilise les optimisations de réduction de taille du compilateur - est-ce une erreur de le faire? il semble plutôt pédant de dire que mon seul choix valable est de le réécrire en assembleur, surtout si cette réécriture fait la même chose que l'optimiseur pour résoudre le problème.
Sdenham

5
@Sdenham Je suppose qu'il y a un peu de place dans la discussion. Si vous n'écrivez plus pour "C ++", mais plutôt pour "Compilateur WindRiver C ++ version 3.4.1", alors je peux voir la logique. Cependant, en règle générale, si vous écrivez quelque chose qui ne fonctionne pas correctement conformément aux spécifications, vous vous retrouvez dans une situation très différente. Je sais que la bibliothèque Boost a un code comme celui-là, mais elle le met toujours en #ifdefblocs et propose une solution de contournement conforme aux normes.
Cort Ammon

4
est-ce une faute de frappe dans le deuxième bloc de code où il est dit b = b + 1?
Stib

2
Vous voudrez peut-être expliquer ce que vous entendez par "machine virtuelle C ++", car ce n'est pas un terme utilisé dans un document standard. Je pense que vous parlez du modèle d’exécution de C ++, mais pas tout à fait certain - et votre terme est faussement similaire à une "machine virtuelle bytecode" qui se rapporte à quelque chose de totalement différent.
Toby Speight

1
@supercat Scala a également une syntaxe de récursion de queue explicite. C ++ est sa propre bête, mais je pense que la récursion de la queue est unidiomatique pour les langages non fonctionnels et obligatoire pour les langages fonctionnels, laissant un petit ensemble de langages où il est raisonnable d'avoir une syntaxe de récursion de la queue explicite. Traduire littéralement la récursion de la queue en boucles et en mutations explicites est tout simplement une meilleure option pour de nombreuses langues.
prosfilaes

8

En pratique, les programmes C ++ attendent quelques optimisations du compilateur.

Recherchez notamment dans les en-têtes standard de vos implémentations de conteneurs standard . Avec GCC , vous pouvez demander le formulaire prétraité ( g++ -C -E) et la représentation interne GIMPLE ( g++ -fdump-tree-gimpleou Gimple SSA avec -fdump-tree-ssa) de la plupart des fichiers source (unités de traduction techniques) à l'aide de conteneurs. Vous serez surpris par la quantité d'optimisation qui est faite (avec g++ -O2). Ainsi, les implémenteurs de conteneurs s'appuient sur les optimisations (et la plupart du temps, l'implémenteur d'une bibliothèque standard C ++ sait quelle optimisation se produirait et écrivait l'implémentation de conteneur en gardant cela à l'esprit; parfois, il écrivait également le passage d'optimisation dans le compilateur. traiter les fonctionnalités requises par la bibliothèque standard C ++).

En pratique, ce sont les optimisations du compilateur qui rendent C ++ et ses conteneurs standard suffisamment efficaces. Vous pouvez donc compter sur eux.

Et de même pour l'affaire RVO mentionnée dans votre question.

La norme C ++ a été co-conçue (notamment en expérimentant des optimisations suffisamment bonnes tout en proposant de nouvelles fonctionnalités) pour bien fonctionner avec les optimisations possibles.

Par exemple, considérons le programme ci-dessous:

#include <algorithm>
#include <vector>

extern "C" bool all_positive(const std::vector<int>& v) {
  return std::all_of(v.begin(), v.end(), [](int x){return x >0;});
}

compilez-le avec g++ -O3 -fverbose-asm -S. Vous découvrirez que la fonction générée n'exécute aucune CALLinstruction machine. Ainsi, la plupart des étapes C ++ (construction d’une fermeture lambda, application répétée, obtention du beginet des enditérateurs, etc.) ont été optimisées. Le code machine ne contient qu'une boucle (qui n'apparaît pas explicitement dans le code source). Sans ces optimisations, C ++ 11 ne réussira pas.

addenda

(ajouté le 31 décembre er 2017)

Voir CppCon 2017: Matt Godbolt «Qu'est-ce que mon compilateur a fait pour moi récemment? Déverrouiller le couvercle du compilateur » .


4

Chaque fois que vous utilisez un compilateur, il est entendu qu’il produira pour vous un code machine ou octet. Cela ne garantit en rien ce que ce code généré est, si ce n'est qu'il implémentera le code source en fonction de la spécification du langage. Notez que cette garantie est la même quel que soit le niveau d'optimisation utilisé et qu'en règle générale, il n'y a donc aucune raison de considérer une sortie comme plus «juste» que l'autre.

De plus, dans les cas, comme RVO, où cela est spécifié dans la langue, il semblerait inutile de faire tout votre possible pour éviter de l’utiliser, en particulier si cela simplifie le code source.

On s’efforce beaucoup de faire en sorte que les compilateurs produisent des résultats efficaces, et l’intention est clairement d’utiliser ces capacités.

Il peut y avoir des raisons pour utiliser du code non optimisé (pour le débogage, par exemple), mais le cas mentionné dans cette question ne semble pas en être un (et si votre code échoue uniquement lorsqu'il est optimisé, il ne s'agit pas d'une conséquence de l’appareil sur lequel vous l’utilisez, alors il ya un bogue quelque part et il est peu probable qu’il soit dans le compilateur.)


3

Je pense que d'autres ont bien abordé l'angle spécifique concernant C ++ et RVO. Voici une réponse plus générale:

Pour ce qui est de l'exactitude, vous ne devriez pas vous fier aux optimisations du compilateur, ni au comportement spécifique du compilateur en général. Heureusement, vous ne semblez pas le faire.

Pour ce qui est des performances, vous devez vous fier au comportement spécifique du compilateur en général et à ses optimisations en particulier. Un compilateur conforme aux normes est libre de compiler votre code comme bon lui semble, à condition que le code compilé se comporte conformément à la spécification du langage. Et je ne suis au courant d'aucune spécification pour un langage grand public spécifiant la rapidité d'exécution de chaque opération.


1

Les optimisations du compilateur ne doivent affecter que les performances, pas les résultats. Compter sur les optimisations du compilateur pour répondre à des exigences non fonctionnelles est non seulement raisonnable, mais souvent aussi la raison pour laquelle un compilateur est sélectionné.

Les indicateurs qui déterminent la manière dont des opérations particulières sont effectuées (conditions d'index ou de dépassement de capacité, par exemple), sont souvent regroupés avec les optimisations du compilateur, mais ne devraient pas l'être. Ils affectent explicitement les résultats des calculs.

Si une optimisation du compilateur entraîne des résultats différents, il s'agit d'un bogue - un bogue dans le compilateur. S'appuyant sur un bug dans le compilateur, est à long terme une erreur - que se passe-t-il quand il est corrigé?

L'utilisation d'indicateurs de compilation qui modifient le fonctionnement des calculs doit être bien documentée, mais utilisée au besoin.


Malheureusement, une grande partie de la documentation du compilateur spécifie mal ce qui est garanti ou non dans les différents modes. De plus, les auteurs de compilateurs "modernes" semblent inconscients des combinaisons de garanties dont les programmeurs ont et n'ont pas besoin. Si un programme fonctionne correctement s'il x*y>zdonne arbitrairement 0 ou 1 en cas de dépassement, à condition qu'il n'ait aucun autre effet secondaire , obliger le programmeur à empêcher les débordements à tout prix ou à forcer le compilateur à évaluer l'expression d'une manière particulière. Inutile altérer les optimisations vs dire que ...
Supercat

... le compilateur peut à son aise se comporter comme s'il x*ypromouvait ses opérandes selon un type plus long et arbitraire (permettant ainsi des formes de levage et de réduction de la résistance qui modifieraient le comportement de certains cas de dépassement de capacité). Cependant, de nombreux compilateurs exigent que les programmeurs empêchent le débordement à tout prix ou obligent les compilateurs à tronquer toutes les valeurs intermédiaires en cas de débordement.
Supercat

1

Non.

C'est ce que je fais tout le temps. Si j'ai besoin d'accéder à un bloc arbitraire de 16 bits en mémoire, je le fais

void *ptr = get_pointer();
uint16_t u16;
memcpy(&u16, ptr, sizeof(u16)); // ntohs omitted for simplicity

... et comptez sur le compilateur qui fera tout ce qui est en son pouvoir pour optimiser ce morceau de code. Le code fonctionne sur ARM, i386, AMD64 et pratiquement sur toutes les architectures existantes. En théorie, un compilateur non optimiseur peut en réalité appeler memcpy, ce qui entraîne des performances totalement mauvaises, mais ce n’est pas un problème pour moi, car j’utilise les optimisations du compilateur.

Considérez l'alternative:

void *ptr = get_pointer();
uint16_t *u16ptr = ptr;
uint16_t u16;
u16 = *u16ptr;  // ntohs omitted for simplicity

Ce code alternatif ne fonctionne pas sur les machines qui nécessitent un alignement correct, si get_pointer()un pointeur non aligné est renvoyé. En outre, il peut y avoir des problèmes d'aliasing dans l'alternative.

La différence entre -O2 et -O0 lors de l'utilisation de l' memcpyastuce est grande: performances de somme de contrôle IP de 3,2 Gbps contre performances de somme de contrôle IP de 67 Gbps. Plus d'un ordre de grandeur!

Parfois, vous devrez peut-être aider le compilateur. Ainsi, par exemple, au lieu de compter sur le compilateur pour dérouler les boucles, vous pouvez le faire vous-même. Soit en implémentant le célèbre appareil de Duff , soit de manière plus propre.

L’inconvénient de s’appuyer sur les optimisations du compilateur est que, si vous exécutez gdb pour déboguer votre code, vous constaterez peut-être que beaucoup de choses ont été optimisées. Donc, vous devrez peut-être recompiler avec -O0, ce qui signifie que les performances seront totalement nul lors du débogage. Je pense que c'est un inconvénient qui vaut la peine d'être pris compte tenu des avantages de l'optimisation des compilateurs.

Quoi que vous fassiez, assurez-vous que votre comportement n’est pas un comportement indéfini. Accéder à un bloc de mémoire aléatoire en tant qu'entier 16 bits est un comportement indéfini en raison de problèmes d'alias et d'alignement.


0

Toutes les tentatives visant à obtenir un code efficace écrit autrement qu'en assemblage reposent énormément sur les optimisations du compilateur, à commencer par l'allocation de registre la plus élémentaire comme efficace pour éviter les débordements superflus de pile et au moins une sélection d'instructions raisonnablement bonne, sinon excellente. Sinon, nous reviendrions aux années 80, où nous devions mettre des registerindices partout et utiliser le nombre minimum de variables dans une fonction pour aider les compilateurs C archaïques, ou même plus tôt, à une époque où l' gotooptimisation des branches était utile.

Si nous n'avions pas le sentiment de pouvoir compter sur la capacité de notre optimiseur à optimiser notre code, nous serions toujours en train de coder des chemins d'exécution critiques pour la performance dans l'assembly.

C’est vraiment une question de fiabilité. Selon vous, l’optimisation peut être mieux réglée en établissant un profil et en examinant les capacités des compilateurs que vous possédez, voire en la désassemblant s’il ya un point chaud dans lequel vous ne pouvez pas savoir où le compilateur semble ont échoué à faire une optimisation évidente.

RVO est quelque chose qui existe depuis des lustres et, à tout le moins en excluant les cas très complexes, les compilateurs s’appliquent bien depuis des lustres. Ce n'est certainement pas la peine de travailler sur un problème qui n'existe pas.

Err sur le côté de s'appuyer sur l'optimiseur, ne pas le craindre

Au contraire, je dirais qu’il ne faut pas trop miser sur l’optimisation du compilateur, et cette suggestion émane de quelqu'un qui travaille dans des domaines très critiques en termes de performances, où efficacité, maintenabilité et qualité perçue par les clients sont primordiales. tous un flou géant. Je préférerais que vous dépendiez trop de votre optimiseur avec confiance et que vous trouviez des cas obscurs où vous vous reposiez trop, plutôt que trop peu et que vous codiez tout le temps à partir de peurs superstitieuses pour le reste de votre vie. Cela vous permettra au moins de rechercher un profileur et d’enquêter correctement si les choses ne s’exécutent pas aussi vite qu’elles le devraient et d’acquérir des connaissances précieuses, et non des superstitions, en cours de route.

Vous vous débrouillez bien pour vous appuyer sur l'optimiseur. Continuez. Ne devenez pas comme ce type qui commence à demander explicitement à toutes les fonctions appelées dans une boucle d'être insérées dans une boucle avant même de vous profiler pour ne pas craindre les faiblesses de l'optimiseur.

Profilage

Le profilage est vraiment le rond-point mais la réponse ultime à votre question. Le problème que les débutants désirant écrire du code efficace ont souvent du mal à résoudre n’est pas ce qu’il faut optimiser, c’est ce qu’il ne faut pas optimiser car ils développent toutes sortes de intuitions erronées concernant des inefficacités qui, tout en étant humainement intuitives, sont fausses en calcul. L’expérience de développement avec un profileur commencera vraiment à vous donner une bonne idée des capacités d’optimisation de vos compilateurs sur lesquelles vous pouvez compter en toute confiance, mais également des capacités (ainsi que des limitations) de votre matériel. Il est sans doute encore plus utile de profiler pour apprendre ce qui ne valait pas la peine d'être optimisé que d'apprendre ce qui était.


-1

Les logiciels peuvent être écrits en C ++ sur des plateformes très différentes et à des fins très diverses.

Cela dépend complètement de l'objectif du logiciel. Devrait-il être facile à maintenir, développer, patcher, refactor et.c. ou bien d’autres facteurs plus importants, tels que les performances, le coût ou la compatibilité avec un matériel spécifique ou le temps qu’il prend pour se développer.


-2

Je pense que la réponse ennuyeuse à cette question est: "ça dépend".

Est-ce une mauvaise pratique d’écrire du code qui repose sur une optimisation du compilateur susceptible d’être désactivée et où la vulnérabilité n’est pas documentée et où le code en question n’est pas testé par unité de sorte que si cela se cassait, vous le sauriez ? Probablement.

Est-ce une mauvaise pratique d’écrire du code reposant sur une optimisation du compilateur peu susceptible d’être désactivée , documentée et testée par unité ? Peut être pas.


-6

À moins qu'il n'y ait plus que vous ne nous dites pas, c'est une mauvaise pratique, mais pas pour la raison que vous suggérez.

Peut-être contrairement aux autres langages que vous avez utilisés auparavant, le renvoi de la valeur d'un objet en C ++ génère une copie de celui-ci. Si vous modifiez ensuite l'objet, vous modifiez un autre objet . C'est-à-dire que si j'ai Obj a; a.x=1;et Obj b = a;, alors je fais b.x += 2; b.f();, alors a.xtoujours égal à 1, pas 3.

Donc non, utiliser un objet comme valeur plutôt que comme référence ou pointeur ne fournit pas les mêmes fonctionnalités et vous pourriez vous retrouver avec des bogues dans votre logiciel.

Peut-être que vous le savez et que cela n’affecte pas votre cas d’utilisation spécifique. Toutefois, selon le libellé de votre question, il semble que vous ne soyez peut-être pas au courant de la distinction; des termes tels que "créer un objet dans la fonction".

"créer un objet dans la fonction" sonne comme new Obj;"retourne l'objet par valeur" sonneObj a; return a;

Obj a;et Obj* a = new Obj;sont des choses très très différentes; le premier peut entraîner une corruption de la mémoire s'il n'est pas utilisé et compris correctement, et le dernier peut entraîner des fuites de mémoire s'il n'est pas utilisé et compris correctement.


8
L'optimisation de la valeur de retour (RVO) est une sémantique bien définie dans laquelle le compilateur construit un objet renvoyé à un niveau supérieur du cadre de la pile, en évitant notamment les copies d'objets inutiles. C'est un comportement bien défini qui a été pris en charge bien avant qu'il ne soit obligatoire dans C ++ 17. Il y a 10 à 15 ans déjà, tous les principaux compilateurs prenaient en charge cette fonctionnalité, de manière cohérente.

@Snowman Je ne parle pas de la gestion physique de la mémoire de bas niveau, et je n'ai pas discuté de la surcharge mémoire ou de la vitesse. Comme je l'ai spécifiquement montré dans ma réponse, je parle des données logiques. Logiquement , fournir la valeur d'un objet en crée une copie, quelle que soit la façon dont le compilateur est implémenté ou l'assembly utilisé en arrière-plan. Le bas niveau des coulisses est une chose, et la structure logique et le comportement du langage en sont une autre; ils sont liés, mais ils ne sont pas la même chose - les deux doivent être compris.
Aaron

6
votre réponse dit "renvoyer la valeur d'un objet en C ++ donne une copie de l'objet", ce qui est complètement faux dans le contexte de RVO - l'objet est construit directement à l'emplacement de l'appelant et aucune copie n'est jamais créée. Vous pouvez tester cela en supprimant le constructeur de la copie et en renvoyant l'objet construit dans l' returninstruction, condition requise pour RVO. En outre, vous abordez ensuite les mots clés newet les pointeurs, ce qui n’est pas l’objet de RVO. Je crois que vous ne comprenez pas la question, ou RVO, ou peut-être les deux.

-7

Pieter B a tout à fait raison de recommander le moins d'étonnement.

Pour répondre à votre question spécifique, ce que cela (le plus probable) signifie en C ++, c'est que vous devez renvoyer a std::unique_ptrà l'objet construit.

La raison en est que cela est plus clair pour un développeur C ++ en ce qui concerne ce qui se passe.

Bien que votre approche fonctionne probablement, vous signalez effectivement que l'objet est un type de valeur faible alors qu'en réalité il ne l'est pas. En plus de cela, vous éliminez toute possibilité d'abstraction d'interface. Cela peut convenir à vos objectifs actuels, mais est souvent très utile lorsque vous utilisez des matrices.

J'apprécie que si vous venez d'autres langues, tous les sigils peuvent être déroutants au début. Mais veillez à ne pas supposer que, en ne les utilisant pas, vous rendez votre code plus clair. En pratique, l'inverse est susceptible d'être vrai.


A Rome, fais comme les Romains.

14
Ce n'est pas une bonne réponse pour les types qui n'effectuent pas eux-mêmes des allocations dynamiques. Le fait que l'OP pense que la chose naturelle dans son cas d'utilisation est de renvoyer par valeur indique que ses objets ont une durée de stockage automatique du côté de l'appelant. Pour des objets simples et pas trop grands, même une implémentation naïve copie-retour-valeur sera beaucoup plus rapide qu'une allocation dynamique. (Si, en revanche, la fonction retourne un conteneur, renvoyer un unique_pointer peut même être avantageux par rapport à un retour de compilateur naïf par la valeur.)
Peter A. Schneider

9
@Matt Si vous ne réalisez pas que ce n'est pas la meilleure pratique. Faire inutilement des allocations de mémoire et imposer une sémantique de pointeur aux utilisateurs est mauvais.
nwp

5
Tout d’abord, lorsqu’on utilise des pointeurs intelligents, il faut retourner std::make_unique, pas std::unique_ptrdirectement. Deuxièmement, RVO n’est pas une optimisation ésotérique, propre au fournisseur: elle est intégrée au standard. Même à l'époque où ce n'était pas le cas, c'était un comportement largement soutenu et attendu. Il ne sert à rien de renvoyer un point std::unique_ptrlorsqu'un pointeur n'est pas nécessaire.

4
@Snowman: Il n'y a pas de "quand ce n'était pas". Bien que cela ne soit devenu obligatoire que récemment , chaque norme C ++ a toujours reconnu [N] RVO et a pris des mesures d'adaptation pour l'activer (par exemple, le compilateur a toujours reçu l'autorisation explicite d'omettre l'utilisation du constructeur de copie sur la valeur de retour, même si a des effets secondaires visibles).
Jerry Coffin
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.