web-dev-qa-db-fra.com

Auto rend-il le code C ++ plus difficile à comprendre?

J'ai vu une conférence par Herb Sutter où il encourage chaque programmeur C++ à utiliser auto.

J'ai dû lire le code C # il y a quelque temps où var était largement utilisé et le code était très difficile à comprendre - chaque fois que var était utilisé, je devais vérifier le type de retour du côté droit. Parfois plus d'une fois, car j'ai oublié le type de la variable après un certain temps!

Je sais que le compilateur connaît le type et je n'ai pas à l'écrire, mais il est largement admis que nous devrions écrire du code pour les programmeurs, pas pour les compilateurs.

Je sais aussi que c'est plus facile à écrire:

auto x = GetX();

Que:

someWeirdTemplate<someOtherVeryLongNameType, ...>::someOtherLongType x = GetX();

Mais ceci n'est écrit qu'une seule fois et le type de retour GetX() est vérifié plusieurs fois pour comprendre quel type x a.

Cela m'a fait me demander: auto rend-il le code C++ plus difficile à comprendre?

127
Mircea Ispas

Réponse courte: Plus complètement, mon opinion actuelle sur auto est que vous devez utiliser auto par défaut sauf si vous voulez explicitement une conversion. (Un peu plus précisément, "... sauf si vous voulez vous engager explicitement dans un type, ce qui est presque toujours parce que vous voulez une conversion.")

Réponse plus longue et justification:

N'écrivez un type explicite (plutôt que auto) que lorsque vous voulez vraiment vous engager explicitement dans un type, ce qui signifie presque toujours que vous voulez obtenir explicitement une conversion vers ce type. Du haut de ma tête, je me souviens de deux cas principaux:

  • (Fréquent) La surprise initializer_list Que auto x = { 1 }; Déduit initializer_list. Si vous ne voulez pas initializer_list, Dites le type - c'est-à-dire, demandez explicitement une conversion.
  • (Rare) La casse des modèles d'expression, telle que auto x = matrix1 * matrix 2 + matrix3; Capture un type d'assistance ou de proxy non destiné à être visible par le programmeur. Dans de nombreux cas, il est bien et bénin de capturer ce type, mais parfois si vous voulez vraiment qu'il s'effondre et fasse le calcul, dites le type - c'est-à-dire, demandez à nouveau explicitement une conversion.

Par défaut, utilisez auto par défaut, car l'utilisation de auto évite les pièges et rend votre code plus correct, plus facile à maintenir et robuste, et plus efficace. Grosso modo du plus important au moins important, dans un esprit "écrivez d'abord pour la clarté et l'exactitude":

  • Exactitude: L'utilisation de auto garantit que vous obtiendrez le bon type. Comme dit le proverbe, si vous vous répétez (dites le type de manière redondante), vous pouvez mentir et vous mentir (vous tromper). Voici un exemple habituel: void f( const vector<int>& v ) { for( /*…* - à ce stade, si vous écrivez explicitement le type de l'itérateur, vous voulez vous rappeler d'écrire const_iterator (L'avez-vous fait?), Tandis que auto fait juste les choses.
  • Maintenabilité et robustesse: L'utilisation de auto rend votre code plus robuste face au changement, car lorsque le type de l'expression change, auto continuera à se résoudre au type correct. Si vous vous engagez à la place sur un type explicite, la modification du type de l'expression injectera des conversions silencieuses lorsque le nouveau type se convertira en ancien type, ou des interruptions de construction inutiles lorsque le nouveau type fonctionne toujours comme l'ancien type mais ne se convertit pas à l'ancien type (par exemple, lorsque vous changez un map en un unordered_map, ce qui est toujours bien si vous ne comptez pas sur la commande, en utilisant auto pour vos itérateurs, vous passez de map<>::iterator à unordered_map<>::iterator en toute transparence, mais l'utilisation de map<>::iterator partout signifie explicitement que vous perdrez votre temps précieux sur une ondulation de correction de code mécanique, à moins qu'un stagiaire ne passe et vous pouvez leur imposer le travail ennuyeux).
  • Performances: Parce que auto garantit qu'aucune conversion implicite ne se produira, il garantit de meilleures performances par défaut. Si au lieu de cela vous dites le type, et qu'il nécessite une conversion, vous obtiendrez souvent silencieusement une conversion, que vous vous y attendiez ou non.
  • Facilité d'utilisation: L'utilisation de auto est votre seule bonne option pour les types difficiles à épeler et inexprimables, tels que les lambdas et les aides de modèle, bref de recourir à des expressions decltype répétitives ou à des indirections moins efficaces comme std::function.
  • Commodité: Et, oui, auto est moins de frappe. Je mentionne ce dernier pour être complet car c'est une raison courante de l'aimer, mais ce n'est pas la plus grande raison de l'utiliser.

Par conséquent: préférez dire auto par défaut. Il offre tellement de simplicité, de performances et de clarté que vous ne vous blessez (ainsi que les futurs responsables de votre code) si vous ne le faites pas. N'engagez un type explicite que lorsque vous le pensez vraiment, ce qui signifie presque toujours que vous voulez une conversion explicite.

Oui, il y a (maintenant) un GotW à ce sujet.

101
Herb Sutter

C'est une situation au cas par cas.

Cela rend parfois le code plus difficile à comprendre, parfois non. Prenez, par exemple:

void foo(const std::map<int, std::string>& x)
{
   for ( auto it = x.begin() ; it != x.end() ; it++ )
   { 
       //....
   }
}

est certainement facile à comprendre et nettement plus facile à écrire que la déclaration réelle de l'itérateur.

J'utilise C++ depuis un certain temps maintenant, mais je peux garantir que j'obtiendrai une erreur de compilation à mon premier coup, car j'oublierais le const_iterator et opterait initialement pour le iterator... :)

Je l'utiliserais pour des cas comme celui-ci, mais pas là où il obscurcit réellement le type (comme votre situation), mais c'est purement subjectif.

112
Luchian Grigore

Regardez-le d'une autre manière. Écris-tu:

std::cout << (foo() + bar()) << "\n";

ou:

// it is important to know the types of these values
int f = foo();
size_t b = bar();
size_t total = f + b;

std::cout << total << "\n";

Parfois, il n'est pas utile de préciser explicitement le type.

La décision de mentionner le type n'est pas la même que la décision de diviser le code sur plusieurs instructions en définissant des variables intermédiaires. En C++ 03, les deux étaient liés, vous pouvez penser à auto comme un moyen de les séparer.

Parfois, rendre les types explicites peut être utile:

// seems legit    
if (foo() < bar()) { ... }

vs.

// ah, there's something tricky going on here, a mixed comparison
if ((unsigned int)foo() < bar()) { ... }

Dans les cas où vous déclarez une variable, l'utilisation de auto permet au type de ne pas être prononcé comme il l'est dans de nombreuses expressions. Vous devriez probablement essayer de décider par vous-même quand cela aide à la lisibilité et quand cela entrave.

Vous pouvez affirmer que mélanger les types signés et non signés est une erreur pour commencer (en fait, certains soutiennent en outre qu'il ne faut pas utiliser du tout les types non signés). La raison c'est sans doute une erreur, c'est qu'elle rend les types d'opérandes d'une importance vitale en raison du comportement différent. Si c'est une mauvaise chose d'avoir besoin de connaître les types de vos valeurs, alors ce n'est probablement pas aussi une mauvaise chose de ne pas avoir besoin de les connaître. Donc, à condition que le code ne soit pas déjà déroutant pour d'autres raisons, cela rend auto OK, non? ;-)

En particulier, lors de l'écriture de code générique, il existe des cas où le type réel d'une variable ne devrait pas être important, ce qui compte, c'est qu'il satisfasse l'interface requise. Donc auto fournit un niveau d'abstraction où vous ignorez le type (mais bien sûr, le compilateur ne le sait pas). Travailler à un niveau d'abstraction approprié peut beaucoup aider à la lisibilité, travailler au "mauvais" niveau fait de la lecture du code un slog.

94
Steve Jessop

OMI, vous regardez cela à peu près à l'envers.

Il ne s'agit pas de auto menant à un code illisible ou encore moins lisible. C'est une question (en espérant que) d'avoir un type explicite pour la valeur de retour compensera le fait que (apparemment) on ne sait pas quel type serait retourné par une fonction particulière.

Du moins à mon avis, si vous avez une fonction dont le type de retour n'est pas immédiatement évident, c'est votre problème ici. Ce que fait la fonction doit être évident d'après son nom, et le type de la valeur de retour doit être évident d'après ce qu'elle fait. Sinon, c'est la véritable source du problème.

S'il y a un problème ici, ce n'est pas avec auto. C'est avec le reste du code, et les chances sont assez bonnes que le type explicite soit juste assez d'un pansement pour vous empêcher de voir et/ou de résoudre le problème principal. Une fois que vous avez résolu ce problème réel, la lisibilité du code en utilisant auto sera généralement très bien.

Je suppose en toute justice que je devrais ajouter: j'ai traité de quelques cas où de telles choses n'étaient pas aussi évidentes que vous le souhaiteriez, et la résolution du problème était également assez intenable. Juste pour un exemple, j'ai fait du conseil pour une entreprise il y a quelques années qui avait auparavant fusionné avec une autre entreprise. Ils se sont retrouvés avec une base de code qui était plus "rapprochée" que vraiment fusionnée. Les programmes constituants avaient commencé à utiliser des bibliothèques différentes (mais assez similaires) à des fins similaires, et bien qu'ils s'efforcent de fusionner les choses plus proprement, ils le faisaient toujours. Dans bon nombre de cas, la seule façon de deviner quel type serait retourné par une fonction donnée était de savoir d'où venait cette fonction.

Même dans un tel cas, vous pouvez aider à clarifier un certain nombre de choses. Dans ce cas, tout le code a commencé dans l'espace de noms global. Le simple fait de déplacer une bonne quantité dans certains espaces de noms a éliminé les conflits de noms et a également facilité le suivi de type.

27
Jerry Coffin

Il y a plusieurs raisons pour lesquelles je n'aime pas l'automobile pour une utilisation générale:

  1. Vous pouvez refactoriser le code sans le modifier. Oui, c'est l'une des choses souvent répertoriées comme un avantage de l'utilisation de l'automobile. Changez simplement le type de retour d'une fonction, et si tout le code qui l'appelle utilise auto, aucun effort supplémentaire n'est requis! Vous appuyez sur compiler, il crée - 0 avertissements, 0 erreurs - et vous allez simplement de l'avant et archivez votre code sans avoir à gérer le désordre de parcourir et de modifier potentiellement les 80 endroits où la fonction est utilisée.

Mais attendez, est-ce vraiment une bonne idée? Et si le type importait dans une demi-douzaine de ces cas d'utilisation, et maintenant que le code se comporte réellement différemment? Cela peut également implicitement rompre l'encapsulation, en modifiant non seulement les valeurs d'entrée, mais le comportement lui-même de l'implémentation privée d'autres classes qui appellent la fonction.

1a. Je suis partisan du concept de "code auto-documenté". Le raisonnement derrière le code auto-documenté est que les commentaires ont tendance à devenir obsolètes, ne reflétant plus ce que fait le code, alors que le code lui-même - s'il est écrit de manière explicite - est explicite, reste toujours à jour sur son intention, et ne vous laissera pas confondu avec des commentaires périmés. Si les types peuvent être modifiés sans avoir à modifier le code lui-même, le code/les variables eux-mêmes peuvent devenir périmés. Par exemple:

auto bThreadOK = CheckThreadHealth ();

Sauf que le problème est que CheckThreadHealth () à un moment donné a été refactorisé pour renvoyer une valeur d'énumération indiquant l'état d'erreur, le cas échéant, au lieu d'un booléen. Mais la personne qui a effectué cette modification n'a pas inspecté cette ligne de code particulière, et le compilateur n'a pas été utile car il a été compilé sans avertissements ni erreurs.

  1. Vous ne saurez peut-être jamais quels sont les types réels. Ceci est également souvent répertorié comme un "avantage" principal de l'automobile. Pourquoi apprendre ce qu'une fonction vous donne, alors que vous pouvez simplement dire: "Qui s'en soucie? Ça compile!"

Cela fonctionne même probablement, probablement. Je dis genre de travaux, car même si vous faites une copie d'une structure de 500 octets pour chaque itération de boucle, afin que vous puissiez inspecter une seule valeur dessus, le code est toujours complètement fonctionnel. Donc, même vos tests unitaires ne vous aident pas à réaliser qu'un mauvais code se cache derrière cette auto simple et innocente. La plupart des autres personnes qui parcourent le fichier ne le remarqueront pas non plus à première vue.

Cela peut également être aggravé si vous ne savez pas quel est le type, mais vous choisissez un nom de variable qui fait une hypothèse erronée sur ce qu'il est, obtenant en fait le même résultat qu'en 1a, mais dès le début plutôt que post-refactor.

  1. Taper le code lors de l'écriture initiale n'est pas la partie la plus longue de la programmation. Oui, auto accélère l'écriture de code au départ. Comme avertissement, je tape> 100 WPM, donc peut-être que cela ne me dérange pas autant que les autres. Mais si tout ce que j'avais à faire était d'écrire du nouveau code toute la journée, je serais un campeur heureux. La partie la plus longue de la programmation consiste à diagnostiquer les bogues difficiles à reproduire dans le code, qui résultent souvent de problèmes subtils non évidents, tels que le type de surutilisation de l'automobile susceptible d'introduire (référence ou copie, signé vs non signé, float vs int, bool vs pointeur, etc.).

Il me semble évident qu'auto a été introduit principalement comme solution de contournement pour une syntaxe épouvantable avec les types de modèles de bibliothèque standard. Plutôt que d'essayer de corriger la syntaxe du modèle que les gens connaissent déjà - ce qui peut également être presque impossible à faire en raison de tout le code existant qu'il pourrait casser - ajoutez un mot-clé qui cache essentiellement le problème. Essentiellement ce que vous pourriez appeler un "hack".

En fait, je n'ai aucun désaccord avec l'utilisation de l'automobile avec les conteneurs de bibliothèque standard. C'est évidemment pour cela que le mot-clé a été créé, et les fonctions de la bibliothèque standard ne sont pas susceptibles de changer fondamentalement de but (ou de type d'ailleurs), ce qui rend l'utilisation de l'automobile relativement sûre. Mais je serais très prudent à l'idée de l'utiliser avec votre propre code et vos interfaces qui peuvent être beaucoup plus volatiles et potentiellement sujettes à des changements plus fondamentaux.

Une autre application utile de l'automobile qui améliore la capacité du langage est la création de temporaires dans des macros de type agnostique. C'est quelque chose que vous ne pouviez pas vraiment faire auparavant, mais vous pouvez le faire maintenant.

19
Tim W

Oui, il est plus facile de connaître le type de votre variable si vous n'utilisez pas auto. La question est: avez-vous besoin de connaître le type de votre variable pour lire le code? Parfois, la réponse sera oui, parfois non. Par exemple, lorsque vous obtenez un itérateur à partir d'un std::vector<int>, avez-vous besoin de savoir que c'est un std::vector<int>::iterator ou serait auto iterator = ...; suffit? Tout ce que quiconque voudrait faire avec un itérateur est donné par le fait qu'il s'agit d'un itérateur - peu importe le type spécifique.

Utilisez auto dans les situations où cela ne rend pas votre code plus difficile à lire.

14
Joseph Mansfield

Personnellement, j'utilise auto uniquement quand c'est absolument évident pour le programmeur de quoi il s'agit.

Exemple 1

std::map <KeyClass, ValueClass> m;
// ...
auto I = m.find (something); // OK, find returns an iterator, everyone knows that

Exemple 2

MyClass myObj;
auto ret = myObj.FindRecord (something)// NOT OK, everyone needs to go and check what FindRecord returns
12
aleguna

Cette question sollicite l'opinion, qui variera d'un programmeur à l'autre, mais je dirais non. En fait, dans de nombreux cas, tout le contraire, auto peut aider à rendre le code plus facile à comprendre en permettant au programmeur de se concentrer sur la logique plutôt que sur les minuties.

Cela est particulièrement vrai face aux types de modèles complexes. Voici un exemple simplifié et artificiel. Qu'est-ce qui est plus facile à comprendre?

for( std::map<std::pair<Foo,Bar>, std::pair<Baz, Bot>, std::less<BazBot>>::const_iterator it = things_.begin(); it != things_.end(); ++it )

.. ou...

for( auto it = things_.begin(); it != things_.end(); ++it )

Certains diront que le second est plus facile à comprendre, d'autres peuvent dire le premier. D'autres encore pourraient dire qu'une utilisation gratuite de auto peut contribuer à une réduction des programmeurs qui l'utilisent, mais c'est une autre histoire.

10
John Dibling

Beaucoup de bonnes réponses jusqu'à présent, mais pour me concentrer sur la question d'origine, je pense que Herb va trop loin dans ses conseils pour utiliser auto généreusement. Votre exemple est un cas où l'utilisation de auto nuit évidemment à la lisibilité. Certaines personnes insistent sur le fait que ce n'est pas un problème avec les IDE modernes où vous pouvez survoler une variable et voir le type, mais je ne suis pas d'accord: même les personnes qui utilisent toujours un IDE ont parfois besoin de regarder des extraits) de code de manière isolée (pensez aux revues de code, par exemple) et un IDE n'aidera pas.

Conclusion: utilisez auto quand cela aide: c'est-à-dire les itérateurs dans les boucles. Ne l'utilisez pas quand cela fait que le lecteur a du mal à trouver le type.

8
Nemanja Trifunovic

Je suis assez surpris que personne n'ait encore souligné que l'auto aide s'il n'y a pas de type clair. Dans ce cas, vous pouvez contourner ce problème en utilisant un #define ou un typedef dans un modèle pour trouver le type réellement utilisable (et ce n'est parfois pas trivial), ou vous utilisez simplement auto.

Supposons que vous ayez une fonction qui renvoie quelque chose avec un type spécifique à la plate-forme:

#ifdef PLATFROM1
__int256 getStuff();
#else //PLATFORM2
__int128 getStuff();
#endif

Quelle utilisation préférez-vous?

#ifdef PLATFORM1
__int256 stuff = getStuff();
#else
__int128 stuff = getStuff();
#endif

ou tout simplement

auto stuff = getStuff();

Bien sûr, vous pouvez écrire

#define StuffType (...)

aussi bien quelque part, mais

StuffType stuff = getStuff();

en fait en dire plus sur le type de x? Il indique que c'est ce qui est retourné à partir de là, mais c'est exactement ce qu'est auto. C'est juste redondant - 'stuff' est écrit 3 fois ici - cela à mon avis le rend moins lisible que la version 'auto'.

6
Lrdx

La lisibilité est subjective; vous devrez examiner la situation et décider de la meilleure solution.

Comme vous l'avez souligné, sans auto, les longues déclarations peuvent produire beaucoup d'encombrement. Mais comme vous l'avez également souligné, de courtes déclarations peuvent supprimer des informations de type qui peuvent être utiles.

En plus de cela, j'ajouterais également ceci: assurez-vous que vous recherchez la lisibilité et non l'écriture. Un code facile à écrire n'est généralement pas facile à lire et vice versa. Par exemple, si j'écrivais, je préférerais auto. Si je lisais, peut-être les déclarations plus longues.

Il y a ensuite la cohérence; est-ce important pour vous? Souhaitez-vous l'auto dans certaines parties et des déclarations explicites dans d'autres, ou une méthode cohérente tout au long?

3
user1161318

Je prendrai l'avantage d'un code moins lisible et encouragerai le programmeur à l'utiliser de plus en plus. Pourquoi? De toute évidence, si le code utilisant auto est difficile à lire, il sera également difficile à écrire. Le programmeur est obligé d'utiliser le nom de variable significatif, pour améliorer son travail.
Au début, le programmeur peut ne pas écrire les noms de variables significatifs. Mais finalement, lors de la correction des bogues, ou lors de la révision du code, quand il/elle doit expliquer le code à d'autres, ou dans un avenir pas si proche, qu'il/elle explique le code aux personnes chargées de la maintenance, le programmeur réalisera l'erreur et utilisera le nom de la variable significative à l'avenir.

2
Manoj R

J'ai deux directives:

  • Si le type de la variable est évident, fastidieux à écrire ou difficile à déterminer, utilisez auto.

    auto range = 10.0f; // Obvious
    
    for (auto i = collection.cbegin(); i != cbegin(); ++i) // Tedious if collection type
    // is really long
    
    template <typename T> ... T t; auto result = t.get(); // Hard to determine as get()
    // might return various stuff
    
  • Si vous avez besoin d'une conversion spécifique ou si le type de résultat n'est pas évident et peut provoquer de la confusion.

    class B : A {}; A* foo = new B(); // 'Convert'
    
    class Factory { public: int foo(); float bar(); }; int f = foo(); // Not obvious
    
2
Red XIII

Oui. Il diminue la verbosité mais le malentendu commun est que la verbosité diminue la lisibilité. Cela n'est vrai que si vous considérez la lisibilité comme esthétique plutôt que votre capacité réelle à interpréter du code - qui n'est pas augmentée en utilisant auto. Dans l'exemple le plus souvent cité, les itérateurs vectoriels, il peut apparaître à la surface que l'utilisation de l'auto augmente la lisibilité de votre code. D'un autre côté, vous ne savez pas toujours ce que le mot-clé auto vous donnera. Vous devez suivre le même chemin logique que le compilateur pour effectuer cette reconstruction interne, et la plupart du temps, en particulier avec les itérateurs, vous allez faire des hypothèses erronées.

À la fin de la journée, `` auto '' sacrifie la lisibilité du code et la clarté, pour la `` propreté '' syntaxique et esthétique (qui n'est nécessaire que parce que les itérateurs ont une syntaxe alambiquée inutilement) et la possibilité de taper peut-être 10 caractères de moins sur une ligne donnée. Cela ne vaut pas le risque ou l'effort à long terme.

0
metamorphosis