9
votes

Existe-t-il un moyen faisant autorité pour se prémunir contre les erreurs "utilisation après déplacement" en C ++?

Je trouve que ces erreurs sont trop faciles à commettre pour les développeurs. Existe-t-il une meilleure pratique ou un moyen faisant autorité pour éviter cela? Existe-t-il un indicateur ou un indice de compilateur qui fonctionne sur plusieurs plates-formes?


15 commentaires

Je ne connais aucun système de compilation pour cela. Je suppose que certains analyseurs statiques le font. D'après mon expérience, un assert d'exécution est suffisant dans la plupart des cas. Modifier: notez que différents types ont des garanties différentes concernant leur état après le déplacement. De nombreux types standard sont parfaitement utilisables, mais leurs valeurs ont été définies sur un état "vide" connu. Il pourrait donc être plus difficile que vous ne le pensez de mettre en œuvre une telle vérification.


Règle simple: ne les laissez pas utiliser std :: move , et s'ils le doivent absolument, mettez cette variable dans une portée afin qu'elle ne soit pas disponible en dehors de la portée dont elle était nécessaire et peut ' t être utilisé.


L'utilisation d'un analyseur statique tel que clang-tidy peut vous aider à trouver les parties problématiques du code. ( clang-tidy: bugprone-use-after -move ).


L'outil n'a pas besoin d'être multiplateforme, mais vous devez le rendre disponible / appliqué dans votre processus de révision de code, dans le cadre, par exemple, de votre CI ou pull request.


Que voulez-vous dire exactement par erreur «utiliser après déplacement»? Lire à partir d'un objet déplacé? Avez-vous des problèmes avec vos propres types ou std ? Si vous possédez des types, vous pouvez mettre l'objet dans un état, qui peut être affirmé par la suite.


Une autre option est de ne pas autoriser du tout std :: move en dehors des listes d'initialisation des membres ou des captures lambda. Si vous avez un cas où vous devez créer un objet, faire des choses dessus, puis le déplacer quelque part, vous pouvez facilement l'encapsuler dans une fonction ou un lambda et avoir la valeur renvoyée à l'objet vers lequel il doit se déplacer et comme ce sera une rvalue, le déplacement se fera automatiquement. Je ne sais pas si c'est vraiment un bon conseil, mais cela vaut peut-être la peine d'être examiné.


En général, je ne pense pas qu'il y ait quelque chose de mal avec use-after-move . Considérez le swap générique commun, tel que: template void swap (T & a, T & b) {T c (std :: move (a)); a = std :: move (b); b = std :: move (c); } ( par exemple, libstdc ++ impl ). Tout d'abord, a est déplacé, puis son opérateur d'attribution de déplacement / copie est appelé (c'est-à-dire que a est utilisé après le déplacement). Voulez-vous que l'outil que vous recherchez vous avertisse de chaque échange?


@DanielLangr Ce n'est pas vraiment une utilisation après le déplacement, du moins pas ce à quoi on se réfère normalement si vous parlez d'utilisateur après le déplacement, vous attribuez un un après vous en être éloigné, mais vous ne l'utilisez pas après bouge toi. Un a.foo () serait un utilisateur après le déplacement.


Êtes-vous en train de poser des questions sur les types std :: qui ne promettent qu'un "état valide mais non spécifié" après avoir été déplacés? Ou d'autres types de bibliothèques où les objets déplacés peuvent être dans des états non valides? Ou vos propres types?


@ t.niese quel est le = , sinon une utilisation? rappelez-vous qu'il est également exprimable comme a.operator = (std :: move (b));


@ t.niese IIRC, vous êtes également autorisé à appeler certaines fonctions membres pour les objets std :: dans un état non spécifié. Tels que std :: vector :: capacity ou :: size . Voir, par exemple, cette réponse: stackoverflow.com/a/9168917/580083 .


@DanielLangr C'est vrai. Mais si une classe est assignable par déplacement et qu'elle se compile, elle doit être valide pour être utilisée (ou la conception de la classe est cassée). Mais pour un a.foo () l'utilisateur a besoin de regarder la documentation, si elle est valide pour appeler la fonction (si elle n'a pas de conditions préalables). Donc, pour une classe correctement implémentée, le compilateur devrait être capable de vous dire si a = std :: move (b); est valide.


"trouve que ces erreurs sont trop faciles à faire pour les développeurs". Je ne pense pas personnellement. Il est beaucoup plus facile d'obtenir une référence suspendue ou de travailler avec un temporaire expiré. Opinion personnelle. Et cela ne veut pas dire qu'il n'est pas intéressant de trouver une solution.


@ t.niese On pourrait en dire autant des constructeurs par défaut. std :: vector ints; ints [0]; est un comportement non défini. Devrions-nous interdire les constructeurs par défaut? ou [] ? Le problème avec "état valide mais non spécifié" est que vous pouvez appeler exactement uniquement les membres sans conditions préalables en toute sécurité.


qu'est-ce que «utiliser après le déplacement»? comment cela peut-il être une erreur? Où se trouve votre exemple reproductible minimal ?


3 Réponses :


2
votes

Le problème est donc vraiment de "lire" après le déplacement. Je pense que je suis d'accord que toute utilisation de std :: move devrait être revue par le code comme un risque potentiel. Si std :: move est à la fin d'une fonction avec une valeur locale ou un paramètre de valeur, tout va bien.

Tout le reste doit être examiné, et toute utilisation de la variable après le déplacement doit faire l'objet d'une attention particulière. Je suppose que donner à la variable un suffixe "_movable" aidera également à la révision du code.

Les quelques cas d'écriture après déplacement, comme le swap, devront simplement être défendus lors de la révision.

Personnellement, je considère toujours std :: move comme une odeur dans le code, un peu comme des lancers.

Je ne suis pas sûr des règles de charpie qui appliqueraient ce modèle, mais je suis sûr qu'elles sont faciles à écrire :-)


0 commentaires

3
votes

Une règle empirique efficace: n'utilisez jamais std :: move ni std :: forward et ne tapez jamais cast vers une référence rvalue (ou universelle). Si vous ne vous déplacez jamais d'une variable ou d'une référence, vous ne pouvez pas faire l'erreur de l'utiliser après. Cette approche a évidemment un inconvénient, puisque ces utilitaires sont utiles dans certains cas pour transformer une copie en un mouvement lorsque le mouvement est suffisant; sans parler des cas où cela est nécessaire.

Approche pour vos propres types: ajoutez des assertions dans les fonctions membres qui vérifient si l'instance a été déplacée et comptez sur elles pour se déclencher pendant les tests. L'état «déplacé» devra être stocké en tant que membre. Les assertions et le membre peuvent être supprimés dans la version de version. Un inconvénient est que cela ajoute un passe-partout inutile à chaque fonction membre.

Une approche supplémentaire: utilisez un outil d'analyse statique qui tente de détecter l'erreur potentielle.

Une règle empirique raisonnable: gardez vos fonctions courtes. Lorsque la fonction est courte, une utilisation sera proche du mouvement, et ainsi l'erreur potentielle est plus facile à repérer.


0 commentaires

1
votes

Interdire std :: move est à courte vue. Le meilleur moyen d'éviter la lecture après le déplacement est de std :: move à la fin de la portée qui a introduit les variables déplacées.


1 commentaires

D'accord, mais devrait vraiment être un commentaire!