Quelle est la raison pour laquelle le nodiscard [[nodiscard]] n'est pas utilisé presque partout dans le nouveau code?

70

C ++ 17 introduit l' [[nodiscard]]attribut, qui permet aux programmeurs de marquer les fonctions de manière à ce que le compilateur génère un avertissement si l'objet renvoyé est ignoré par un appelant. le même attribut peut être ajouté à un type de classe entier.

J'ai lu des explications sur la motivation de cette fonctionnalité dans la proposition d' origine et je sais que C ++ 20 ajoutera l'attribut à des fonctions standard telles que std::vector::empty, dont les noms ne transmettent pas une signification non ambiguë concernant la valeur de retour.

C'est une fonctionnalité intéressante et utile. En fait, cela semble presque trop utile. Partout où je lis [[nodiscard]], les gens en discutent comme si vous ajoutiez simplement cela à quelques fonctions ou types et oubliez le reste. Mais pourquoi une valeur non éliminable devrait-elle constituer un cas particulier, en particulier lors de l'écriture d'un nouveau code? Une valeur de retour supprimée n'est-elle pas généralement un bogue ou du moins un gaspillage de ressources?

Et l’un des principes de conception de C ++ n’est-il pas que le compilateur doit intercepter autant d’erreurs que possible?

Si tel est le cas, pourquoi ne pas ajouter [[nodiscard]]votre propre code, non hérité, à presque chaque voidtype de classe et à chaque type de fonction?

J'ai essayé de le faire dans mon propre code, et cela fonctionne bien, sauf que c'est tellement terriblement prolixe que ça commence à ressembler à Java. Il semblerait beaucoup plus naturel de mettre les compilateurs en garde contre les valeurs renvoyées par défaut, à l' exception des rares cas où vous indiquez votre intention [*] .

Comme je n’ai vu aucune discussion sur cette possibilité dans les propositions standard, les entrées de blog, les questions Stack Overflow ou ailleurs sur Internet, il me manque quelque chose.

Pourquoi une telle mécanique n'aurait-elle pas de sens dans le nouveau code C ++? La verbosité est-elle la seule raison de ne pas utiliser [[nodiscard]]presque partout?


[*] En théorie, vous pouvez avoir quelque chose comme un [[maydiscard]]attribut, qui pourrait aussi être ajouté rétroactivement à des fonctions comme printfdans les implémentations de bibliothèques standard.

Christian Hackl
la source
22
Il existe de nombreuses fonctions dans lesquelles la valeur de retour peut être raisonnablement ignorée. operator =par exemple. Et std::map::insert.
user253751
2
@immibis: Vrai. La bibliothèque standard est pleine de telles fonctions. Mais dans mon propre code, ils ont tendance à être extrêmement rares; Pensez-vous que c'est une question de code de bibliothèque par rapport au code d'application?
Christian Hackl
9
"... terriblement commenté qu'il commence à ressembler à Java ..." - lire ceci dans une question sur le C ++ me fit un peu contracter: - /
Marco13
3
@ChristianHackl Les commentaires ne sont probablement pas le bon endroit pour "dénigrer les langues", et bien sûr, Java est aussi prolixe que d'autres langages. Mais les fichiers d' en- tête, les opérateurs d'affectation, les constructeurs de copie et de l' utilisation correcte de constpeut enfler une autre « simple » classe (ou plutôt « bon vieux objet de données ») considérablement en C ++.
Marco13
2
@ Marco13: Je ne peux pas aborder autant de points dans un commentaire. Soyons brefs: Java n'a pas de fichiers d'en-tête, ce qui réduit le nombre de fichiers mais augmente le couplage. OTOH vous oblige à mettre chaque classe de niveau supérieur dans son propre fichier et chaque fonction dans une classe. En C ++, vous n'implémentez presque jamais d'opérateurs d'affectation et de constructeur de copie vous-même; ces fonctions sont plus pertinentes pour les classes de bibliothèque standard telles que std::vectorou std::unique_ptrdont vous avez simplement besoin de données membres dans votre définition de classe. J'ai travaillé avec les deux langues. Java est une langue okayish, mais plus verbeuse.
Christian Hackl

Réponses:

73

Dans les nouveaux codes qui ne doivent pas nécessairement être compatibles avec les normes plus anciennes, utilisez cet attribut chaque fois que vous le souhaitez. Mais pour C ++, [[nodiscard]]fait un mauvais défaut. Vous suggérez:

Il semblerait beaucoup plus naturel de mettre les compilateurs en garde contre les valeurs renvoyées par défaut, à l'exception des rares cas où vous indiquez votre intention.

Cela provoquerait soudainement un code correct existant qui émettrait de nombreux avertissements. Bien qu'un tel changement puisse techniquement être considéré comme rétrocompatible étant donné que tout code existant est toujours compilé avec succès, ce serait un énorme changement de sémantique dans la pratique.

Les décisions de conception pour un langage mature existant avec une très grande base de code sont nécessairement différentes des décisions de conception pour un langage complètement nouveau. S'il s'agissait d'une nouvelle langue, il serait judicieux d'avertir par défaut. Par exemple, le langage Nim exige que les valeurs inutiles soient ignorées explicitement - ce qui serait similaire à l'encapsulation de chaque instruction d'expression en C ++ (void)(...).

Un [[nodiscard]]attribut est le plus utile dans deux cas:

  • si une fonction n'a pas d'effet au-delà d'un résultat donné, c'est-à-dire pure. Si le résultat n'est pas utilisé, l'appel est certainement inutile. D'autre part, ignorer le résultat ne serait pas incorrect.

  • si la valeur de retour doit être vérifiée, par exemple pour une interface de type C qui renvoie des codes d'erreur au lieu d'être générée. C'est le cas d'utilisation principal. Pour le C ++ idiomatique, cela va être assez rare.

Ces deux cas laissent un immense espace de fonctions impures qui retournent une valeur, où un tel avertissement serait trompeur. Par exemple:

  • Considérons un type de données de file d'attente avec une .pop()méthode qui supprime un élément et renvoie une copie de l'élément supprimé. Une telle méthode est souvent pratique. Cependant, dans certains cas, nous souhaitons uniquement supprimer l'élément sans en obtenir une copie. C'est parfaitement légitime, et un avertissement ne serait pas utile. Une conception différente (telle que std::vector) divise ces responsabilités, mais comporte d'autres compromis.

    Notez que dans certains cas, une copie doit être faite de toute façon, donc, grâce à RVO, le retour de la copie serait gratuit.

  • Envisagez des interfaces fluides, où chaque opération renvoie l'objet afin que d'autres opérations puissent être effectuées. En C ++, l'exemple le plus courant est l'opérateur d'insertion de flux <<. Il serait extrêmement fastidieux d’ajouter un [[nodiscard]]attribut à chaque <<surcharge.

Ces exemples montrent que rendre la compilation de code C ++ idiomatique sans avertissements dans un langage «C ++ 17 avec nodiscard par défaut» serait assez fastidieux.

Notez que votre tout nouveau code C ++ 17 (où vous pouvez utiliser ces attributs) peut toujours être compilé avec des bibliothèques qui ciblent des normes C ++ plus anciennes. Cette compatibilité en amont est cruciale pour l'écosystème C / C ++. Par conséquent, si vous définissez nodiscard comme valeur par défaut, de nombreux avertissements seraient utilisés dans des cas d'utilisation idiomatiques typiques, avertissements que vous ne pouvez pas corriger sans modifier de manière approfondie le code source de la bibliothèque.

On peut soutenir que le problème ici n’est pas le changement de sémantique, mais que les fonctionnalités de chaque norme C ++ s’appliquent à une étendue par unité de compilation et non à une étendue par fichier. Si / quand une future norme C ++ s'éloigne des fichiers d'en-tête, une telle modification serait plus réaliste.

Amon
la source
6
J'ai voté pour cela, car il contient des informations utiles. Pas encore sûr de savoir si cela répond vraiment à la question. Les fonctions impures comme popou operator<<, celles-ci seraient explicitement marquées par mon [[maydiscard]]attribut imaginaire , ainsi aucun avertissement ne serait généré. Êtes-vous en train de dire que de telles fonctions sont généralement beaucoup plus courantes que ce que j'aimerais croire, ou beaucoup plus courantes en général que dans mes propres bases de code? Votre premier paragraphe sur le code existant est certes vrai, mais je me demande tout de même si quelqu'un d'autre utilise actuellement tout son nouveau code [[nodiscard]], et si ce n'est pas le cas, pourquoi pas.
Christian Hackl
23
@ChristianHackl: Il fut un temps dans l'histoire de C ++ où il était considéré comme une bonne pratique de renvoyer des valeurs en tant que const. Vous ne pouvez pas dire returns_an_int() = 0, alors pourquoi devrait returns_a_str() = ""travailler? C ++ 11 a montré que c'était en fait une idée terrible, car maintenant tout ce code interdisait les déplacements, car les valeurs étaient const. Je pense que la leçon à retenir de cette leçon est "ce n'est pas à vous de voir ce que vos appelants font avec votre résultat". Ignorer le résultat est une chose tout à fait valable que les appelants pourraient vouloir faire et, à moins que vous sachiez que c'est faux (une barre très haute), vous ne devriez pas le bloquer.
GManNickG
13
Une nodiscard empty()est également utile car son nom est assez ambigu: est-ce un prédicat is_empty()ou un mutateur clear()? Vous ne vous attendriez généralement pas à ce qu'un tel mutateur retourne quoi que ce soit, donc un avertissement concernant une valeur de retour peut alerter le programmeur d'un problème. Avec un nom plus clair, cet attribut ne serait pas aussi nécessaire.
amon
13
@ChristianHackl: Comme d'autres l'ont dit, je pense que les fonctions pures constituent un bon exemple du moment où cela devrait être utilisé. Je voudrais aller un peu plus loin et dire qu'il devrait y avoir un [[pure]]attribut, et cela devrait impliquer [[nodiscard]]. De cette façon, il est clair que ce résultat ne doit pas être ignoré. Mais à part les fonctions pures, je ne peux pas penser à une autre classe de fonctions qui devrait avoir ce comportement. Et comme la plupart des fonctions ne sont pas pures, je ne pense pas que nodiscard par défaut soit le bon choix.
GManNickG
5
@GManNickG: Après avoir tenté et échoué de déboguer un code ignorant les codes d'erreur, je suis fermement convaincu que la grande majorité des codes d'erreur doivent être vérifiés par défaut.
Mooing Duck
9

Raisons pour lesquelles je n’aurais pas [[nodiscard]]presque partout:

  1. (majeur :) Cela introduirait beaucoup trop de bruit dans mes en-têtes.
  2. (majeur :) Je ne pense pas que je devrais faire de fortes hypothèses spéculatives sur le code des autres. Vous voulez jeter la valeur de retour que je vous ai donnée? Bien, assommez-vous.
  3. (mineur :) Vous garantiriez une incompatibilité avec C ++ 14

Maintenant, si vous le définissiez par défaut, vous obligeriez tous les développeurs de bibliothèques à obliger tous leurs utilisateurs à ne pas ignorer les valeurs renvoyées. Ce serait horrible. Ou vous les obligez à ajouter [[maydiscard]]d'innombrables fonctions, ce qui est également assez horrible.

einpoklum - réintègre Monica
la source
0

Par exemple: l'opérateur << a une valeur de retour qui dépend de l'appel, soit absolument nécessaire, soit absolument inutile. (std :: cout << x << y, le premier est nécessaire car il renvoie le flux, le second est inutile). Maintenant, comparons avec printf où tout le monde rejette la valeur de retour qui est là pour la vérification d'erreur, mais l'opérateur << n'a pas d'erreur de vérification et n'a donc pas pu être aussi utile en premier lieu. Donc, dans les deux cas, Nodiscard serait juste dommageable.

gnasher729
la source
Cela répond à une question qui n'a pas été posée, à savoir "Quelle est la raison pour laquelle vous n'utilisez pas [[nodiscard]]partout?".
Christian Hackl