8
votes

Est-il raisonnable de prendre std :: istream && comme argument?

J'ai rencontré du code qui fait ceci:

SomeObject parse (std::istream && input) {....

L'argument input est une référence rvalue, ce qui signifie normalement que la fonction est destinée à prendre possession de l'argument . Ce n'est pas tout à fait ce qui se passe ici.

La fonction parse consommera complètement le flux d'entrée, et elle exigera une référence rvalue, car alors l'appel du code donnera la propriété du istream et donc c'est un signal que le flux d'entrée sera inutilisable.

Je pense que c'est correct car, puisque la fonction parse ne bouge pas réellement l'objet autour, il n'y a aucun danger de découper le sous-type. Cela se comporte essentiellement comme une référence normale du point de vue de parse , mais il y a une sorte de commentaire compilable à la fonction appelante que vous devez abandonner la propriété du flux.

Ce code est-il réellement sûr? Ou y a-t-il une subtilité négligée qui rend cela dangereux?


22 commentaires

Les juristes du langage C ++ auront sans aucun doute quelques choses à dire à ce sujet. En attendant, pouvez-vous définir les termes «d'accord», «sûr», «raisonnable» et «dangereux» dans le contexte de votre question, s'il vous plaît? Le cognac d'un homme est le poison d'un autre homme.


À l'abri de quoi ? OK par qui? Dangereux par quoi?


Il me semble que ce n'est qu'un moyen de forcer ce paramètre à toujours être temporaire.


Votre objectif avec SomeObject parse (std :: istream && input) est d'obtenir la fonction pour n'accepter que les temporaires?


@SamVarshavchik ou pour forcer un programmeur à std :: move le flux d'entrée sur lequel il a travaillé, ce qui indiquera que personne ne devrait essayer d'en extraire par la suite.


Cela ne semble pas "dangereux" étant donné l'état actuel de l'interface std :: istream . Mais la sémantique ici est à mon humble avis discutable. Que signifie pour le code d'appel «céder la propriété» mais en même temps «ne pas la transférer»? Qui "possède" le flux après le retour de parse () !? De quelle manière exactement parse () rend-il le flux "inutilisable"? Que faire si l'analyse échoue en raison d'une erreur avant que le flux entier ne soit «consommé»? Le flux est-il "inutilisable" alors !? La «propriété» est-elle en quelque sorte «rendue» au code d'appel dans ce cas?


Cela vaut-il le facteur WTF d'un appelant quelque part sur la ligne forcé d'écrire parse (std :: move (std :: cin)) ?


@Caleth Considérez qu'il pourrait très bien être utilisé avec d'autres flux, et prase (std :: move (my_string_stream)); est beaucoup moins choquant.


@Caleth et après le "WTF" initial, vous commencez à comprendre que "oh, donc il analysera toute l'entrée, puisque je renonce à la propriété de std :: cin et que je ne devrais pas l'utiliser afterwads. Compris. Neat ". Au moins dans ma perception. Considérez également l'utilisation des s std :: stringstream et std :: fstream .


Alors, que peut-il faire pour rendre std :: cin inutilisable? Cet aspect n'a en particulier aucun sens pour moi étant donné le concept même de flux. Un flux peut être à EOF. Cela ne rend pas cet objet de flux inutilisable. Un flux peut être en mauvais état. Cela ne rend pas l'objet inutilisable…


@MichaelKenzel tout ce que le programmeur de parse veut faire dans le monde entier. Ce n'est pas prometteur que "je détruirai tellement ce flux que vous ne pourrez même pas reconnaître ce qui reste". Il exprime que "je veux posséder ce flux, en faire ce que je veux et vous ne l'utiliserez pas pour autre chose que l'assigner".


Mais quel est l'intérêt d'introduire cette restriction arbitraire à l'interface alors qu'il n'y a littéralement rien qu'une implémentation puisse faire pour "en profiter" !?


Pour indiquer qu'il lira tout. Il ne s'agit pas de tirer parti des références rvalue. Il s'agit d'utiliser des références rvalue pour exprimer l'intention d'une manière très intelligente qui permet d'effectuer une vérification au moment de la compilation.


En passant une référence à un std :: istream non-const, l'appelant doit déjà supposer que celui qui reçoit le flux lira à partir de celui-ci. S'il lit au-delà de la fin du flux, le flux sera dans un état EOF. Si une erreur se produit, le flux sera dans un mauvais état. En aucun cas l'objet de flux lui-même ne sera dans un état "inutilisable". C'est tout le concept d'un std :: istream


«manière très intelligente» que certains d’entre nous trouvent déroutante . Et ce n'est pas une bonne vérification, car vous pouvez assez facilement mentir , voir parse (std :: move (std :: cin))


Bien que ce ne soit pas parfait (et que beaucoup de discussions pourraient se déclencher, puisque c'est en fait la première fois que je vois quelque chose comme ça) et potentiellement difficile à «obtenir», je pense toujours que cela exprime très clairement l'intention. De plus, je ne considère pas parse (std :: move (std :: cin)) comme un mensonge . Allez-y et analysez tout de std :: cin . Je pourrais évidemment faire quelque chose avec après (vous pouvez faire à peu près tout en C ++ ), mais je sais que je ne devrais pas. Si vous voulez lire à partir de std :: cin d'une autre manière que parse , alors ... n'utilisez pas parse ?


Et s'il s'agissait de quelque chose de plus qu'un tableau régulier de caractères ou d'un fichier. Cela pourrait être, par exemple, une connexion réseau, ou un tube d'entrée-sortie, ou ... Ce n'est pas parce que parse lira tout, cela ne signifie pas qu'il devient inutile pour l'appelant. Il pourrait, par exemple, faire quelque chose de manière asynchrone dans un autre thread. Par exemple, les lectures à partir du flux peuvent bloquer le thread actuel, en attendant qu'un autre thread écrive sur le même objet. Mais si vous le forcez à être std :: move -d, vous indiquez qu'il ne devrait pas être utilisé de cette façon.


Je vais le répéter une fois de plus, car j'ai déjà fourni la réponse à cette question - si parse fait quelque chose que vous ne voulez pas faire, alors simplement n'utilisez pas < code> parse .


@Fureeish Si vous vous référez à mes exemples - je ne vois pas parse faire quoi que ce soit que je ne veux pas qu'il fasse, sauf pour prendre mon istream par référence à la valeur r - et c'est la question du PO - ou du moins proche de lui - "est-ce que ça va?", ou comme je l'interprète - "est-ce que je perds quelque chose en faisant cela?"


parse pourrait engendrer un thread qui attendra n'importe quel signal d'un flux donné et dans ce cas vous voudrez vous assurer que personne d'autre n'utilisera jamais ce flux . Il y a quelques exemples, mais je pense que ce n'est pas le meilleur endroit pour discuter de cette idée. Vous avez certainement des points valables et je penche vers votre position, mais j'ai encore des doutes. Attendons d'autres réponses possibles.


Quoi que fasse parse , la prémisse de la question est qu'elle rend le flux illisible. Si vous ne pouvez pas accepter que la prémisse soit vraie, remplacez std :: istream par foo qui peut être tout ce qui est compatible avec la prémisse.


@Fureeish Attention cependant. Cet exemple n'est peut-être pas approprié ici. S'il effectue des opérations asynchrones, il doit soit s'approprier le flux d'une manière ou d'une autre pour préserver sa durée de vie, soit l'appelant de la fonction a le fardeau énorme de faire en sorte que le flux survit à l'opération asynchrone.


4 Réponses :


6
votes

Un std :: istream n'est pas déplaçable , il n'y a donc aucun avantage pratique à ceci.

Cela signale déjà que l'objet peut être "modifié", sans la confusion de suggérer que vous transférez la propriété de l'objet std :: istream (ce que vous ne faites pas et que vous pouvez ' t faire).

Je peux en quelque sorte voir le raisonnement derrière l'utilisation de ceci pour dire que le flux est logiquement déplacé, mais je pense que vous devez faire une distinction entre "la propriété de cette chose est en cours de transfert", et "Je garde la propriété de cette chose mais je vous laisserai consommer tous ses services". Le transfert de propriété est assez bien compris comme une convention en C ++, et ce n'est pas vraiment ça. Que pensera un utilisateur de votre code lorsqu'il devra écrire parse (std :: move (std :: cin)) ?

Votre chemin n'est cependant pas "dangereux"; vous ne pourrez rien faire avec cette référence rvalue que vous ne pouvez pas faire avec une référence lvalue.

Il serait beaucoup plus auto-documenté et conventionnel de prendre simplement une référence à lvalue.


9 commentaires

Prendre la référence rvalue et se déplacer n'est pas nécessairement lié l'un à l'autre.


@SergeyA Ne pas voir le but d'une référence rvalue si vous n'allez pas transférer la propriété via un déplacement (et que vous ne voulez pas limiter les appels aux expressions rvalue pour des raisons de surcharge arcane). Pouvez-vous donner un exemple de quand cela serait utile?


" Pouvez-vous donner un exemple ", eh bien, le code dans la question ... Au moins à mon avis, le fait que l'on va transférer la propriété du flux d'entrée ( ce qui signifie en passant par rvalue reference) indiquera que personne ne devrait essayer d'en extraire par la suite, ce que je considère comme une manière élégante d'utiliser la syntaxe C ++ pour exprimer l'intention. Mais c'est juste mon opinion.


Vous en avez donné un vous-même - vous voulez que votre appel soit limité aux valeurs. Par exemple, (je ne dis pas ce cas OP, je pense que la question n'est pas claire et fermable) on pourrait vouloir demander que l'objet fourni à la fonction soit un "bloc-notes". Je l'utilise moi-même dans un certain cas.


Si la fonction doit rendre l'objet reçu inutilisable, elle pourrait tout aussi bien mettre fin à sa durée de vie. Vous ne pouvez pas réellement vous déplacer dans une fonction de récepteur, vous pouvez simplement mettre cet objet dans un état de fonctionnalité limité (similaire ou identique à un état déplacé). Mais fonctionnellement, il semble exactement avoir pris possession de l'objet. D'une certaine manière, même s'il ne conserve pas cette propriété.


Le transfert de propriété est un contexte assez bien défini en C ++ et implique généralement un déplacement. Vous ne pouvez tout simplement pas faire cela avec un std :: istream . Vous pouvez faire en sorte que votre fonction n'accepte qu'une rvalue pour signifier qu'elle va lire toutes les données du flux, bien sûr, mais ce n'est pas ce que le transfert de propriété a jamais signifié en C ++, donc ce serait assez étrange IMO! Je veux dire que je peux en quelque sorte le voir, mais je ne vois pas que cela en vaut la peine.


Êtes-vous sûr de ne pas pouvoir déplacer les std :: istream ? Cela suggère le contraire et je suis assez certain que j'ai fait ceci avant.


@LightnessRacesinOrbit Il y a des endroits dans la bibliothèque standard où une valeur x est prise, mais pas nécessairement déplacée, par exemple std :: string operator + (std :: string &&, std :: string &&) (je sais que c'est en fait un modèle, je suis KISS.) En tant que tel, je ne pense pas que rvalues ​​ impliquent le déplacement.


@templatetypedef Uniquement à partir de types dérivés - le cteur de déplacement est protégé.



6
votes

std :: move produit simplement une référence rvalue à partir d'un objet et rien de plus. La nature d'une rvalue est telle que vous pouvez supposer que personne d'autre ne se souciera de son état une fois que vous en avez terminé. std :: move est alors utilisé pour permettre aux développeurs de faire cette promesse sur les objets avec d'autres catégories de valeurs. En d'autres termes, appeler std :: move dans un contexte significatif équivaut à dire "Je promets que je ne me soucie plus de l'état de cet objet".

Puisque vous allez rendre l'objet essentiellement inutilisable et que vous voulez vous assurer que l'appelant n'utilisera plus l'objet en utilisant une référence rvalue applique cette attente dans une certaine mesure. Cela oblige l'appelant à faire cette promesse à votre fonction. Le fait de ne pas tenir la promesse entraînera une erreur du compilateur (en supposant qu'il n'y ait pas d'autre surcharge valide). Peu importe que vous vous déplaciez réellement de l'objet ou non, seulement que le propriétaire d'origine a accepté de renoncer à sa propriété.


1 commentaires

Surtout d'accord, mais je ne vois toujours pas pourquoi vous voudriez faire cela à un flux. C'est inutilement contraignant à moins qu'il y ait des contraintes de domaine que l'OP n'a pas mentionnées. La réponse de Michael l'exprime assez bien.



2
votes

Ce que vous essayez de faire ici n'est pas «dangereux» dans le sens où, étant donné l'interface actuelle std :: istream , il ne semble pas y avoir de circonstances dans lesquelles un rvalue référence ici conduirait nécessairement à un comportement indéfini lorsque la prise d'une référence lvalue n'aurait pas. Mais la sémantique de tout cet engin est à mon humble avis au mieux très discutable. Que signifie pour le code d'appel «céder la propriété» mais en même temps «ne pas la transférer»? Qui "possède" le flux après le retour de parse () !? De quelle manière exactement parse () rend-il le flux "inutilisable"? Que faire si l'analyse échoue en raison d'une erreur avant que le flux entier ne soit «consommé»? Le flux est-il "inutilisable" alors !? Personne n'est autorisé à essayer de lire le reste? La «propriété» est-elle en quelque sorte «rendue» au code d'appel dans ce cas?

Un flux est un concept abstrait. Le but de l'abstraction de flux est de servir d'interface à travers laquelle quelqu'un peut consommer des entrées sans avoir à savoir d'où proviennent les données, vivent ou comment elles sont accessibles et gérées. Si le but de parse () est d'analyser les entrées provenant de sources arbitraires, alors il ne devrait pas être concerné par la nature de la source. S'il s'intéresse à la nature de la source, il devrait alors demander un type de source spécifique. Et c'est là, à mon humble avis, que votre interface se contredit. Actuellement, parse () prend une source arbitraire. L'interface dit: je prends le flux que vous me donnez, peu m'importe comment il est implémenté. Tant que c'est un flux, je peux travailler avec. En même temps, il oblige l'appelant à abandonner l'objet qui implémente réellement le flux. L'interface nécessite que l'appelant remette quelque chose que l'interface elle-même empêche toute implémentation derrière l'interface de connaître, d'accéder ou d'utiliser de quelque manière que ce soit. Par exemple, comment puis-je lire parse () à partir d'un std :: ifstream ? Qui ferme le dossier après? Si ne peut pas être l'analyseur. Cela ne peut pas non plus être moi, car appeler l'analyseur m'a obligé à remettre l'objet. En même temps, je sais que l'analyseur ne pourrait même jamais savoir qu'il devait fermer le fichier que j'ai remis…

Il fera toujours la bonne chose à la fin car il n'y avait aucun moyen qu'une implémentation de l'interface ait pu réellement faire ce que l'interface suggérait de faire et donc mon std :: ifstream destructor exécutera et fermera simplement le fichier. Mais qu'avons-nous gagné exactement en nous mentant comme ça!? Vous avez promis de prendre en charge l'objet quand vous n'alliez jamais, j'ai promis de ne plus jamais toucher l'objet alors que je savais que je devrais toujours le faire…


0 commentaires

2
votes

Votre hypothèse selon laquelle le paramètre de référence rvalue implique "s'approprier" est complètement incorrecte. La référence Rvalue est juste un type de référence spécifique, qui vient avec ses propres règles d'initialisation et de résolution de surcharge. Ni plus ni moins. Formellement, il n'a aucune affinité particulière avec le «déplacement» ou la «prise de possession» de l'objet référencé.

Il est vrai que la prise en charge de la sémantique de déplacement est considérée comme l’un des principaux objectifs des références rvalue, mais vous ne devez pas supposer que c’est leur seul objectif et ces caractéristiques sont en quelque sorte inséparables. Comme toute autre fonctionnalité du langage, il peut permettre un nombre important d'utilisations idiomatiques alternatives bien développées.

Un exemple de citation similaire à ce que vous venez de citer est en fait présent dans la bibliothèque standard elle-même. Ce sont les surcharges supplémentaires introduites dans C ++ 11 (et C ++ 17, selon certaines nuances)

#include <string>
#include <sstream>

int main() 
{
  std::string s;
  int a;

  std::istringstream("123 456") >> a >> s;
  std::istringstream("123 456") >> s >> a;
  // Despite the obvious similarity, the first line is well-formed in C++03
  // while the second isn't. Both lines are well-formed in C++11
}

Leur objectif principal est de «combler» la différence comportement entre les surcharges membres et non membres de l'opérateur

template< class CharT, class Traits, class T >
basic_ostream< CharT, Traits >& operator<<( basic_ostream<CharT,Traits>&& os, 
                                            const T& value );

template< class CharT, class Traits, class T >
basic_istream<CharT,Traits>& operator>>( basic_istream<CharT,Traits>&& st, T&& value );

Il tire parti du fait que la référence rvalue peut se lier à des objets temporaires et les voir comme des objets modifiables . Dans ce cas, la référence rvalue est utilisée à des fins qui n'ont rien à voir avec la sémantique de déplacement. C'est parfaitement normal.


1 commentaires

Quels sont ces objectifs? Je pense que c'est le point clé de cette question. Si vous pouvez expliquer à quoi cela sert dans cet exemple et comment cela se rapporte à la situation du PO, c'est probablement la réponse.