À propos du « sain usage » du mot clé auto

Quelques raccourcis :

Une version anglaise de ce qui suit est disponible ici. Je ne fais pas souvent de version bilingue des textes sur ce site, mais puisque ce qui suit est né d'échanges avec des amis anglophones, je souhaitais leur permettre de lire le tout.

Ce qui suit indique ma pensée actuelle à propos du mot clé auto lorsqu'il est utilisé pour définir une variable. Je suis conscient que c'est une pensée embryonnaire, encore en évolution, mais tant mieux si cela peut contribuer au débat.

Notez que le mot clé auto sert aujourd'hui à plusieurs autres fins que celles décrites ici, incluant le type de retour de fonctions et leurs paramètres. Nous n'approfondirons pas ces autres sujets ici, faute de temps de la part de l'auteur.

Le mot clé auto, traditionnellement réservé pour signifier ce qu'on pourrait vulgariser comme « variable locale » (allocation automatique, sur la pile, par opposition à static) et traditionnellement inutilisé car essentiellement redondant, permet depuis C++ 11 d'exprimer, du moins lors de la définition d'une variable, « le type de la variable est celui de l'expression utilisée pour l'initialiser » (moins les références et les qualifications const et volatile).

Par exemple :

static const int glob = 3;
int &f() { return glob; }
int g() { return glob; }
int main() {
   int i0 = glob;       // i0 est un int et est initialisé avec une copie de la valeur de glob
   auto i1 = glob;      // i1 est un int (pas const) et est initialisé avec une copie de la valeur de glob
   auto i2 = f();       // i2 est un int (pas un int&!) et est initialisé avec une copie de la valeur de glob
   auto i3 = 0.5 + g(); // i3 est un double car l'expression 0.5 + g() est de type double
}

Plusieurs experts débattent des vertus et des vices de déclarer une variable avec auto. Il existe au moins trois camps :

Source

La position AAA

La position AAA est à l'effet qu'utiliser auto pour définir une variable devrait être le réflexe des programmeuses et des programmeurs. Cette position met de l'avant que l'écriture résultante est homogène, et qu'on ne peut oublier d'initialiser les variables en procédant ainsi.

Sans auto Avec auto Remarques
int i = 0;
auto i = 0;
// ou encore
auto i = int{};
// ou encore
auto i = int();
// ou encore
auto i = int{0};

Comme la plupart des gens, je n'utiliserais pas auto ici, mais un tenant de l'approche AAA ferait remarquer que ceci :

auto i = int();

... compile et initialise i à zéro, alors que cela :

int i();

... compile mais peut surprendre en déclarant une fonction nommée i, ne prenant pas de paramètre et retournant un int.

std::vector<std::string> v = { "J'aime", "mon", "prof" };
for (std::vector<std::string>::const_iterator it = v.cbegin(); it != v.cend(); ++it) {
   std::cout << *it << ' ';
}
// ou encore
for (const std::string &s : v) {
   std::cout << s << ' ';
}
std::vector<std::string> v = { "J'aime", "mon", "prof" };
for (auto &it = v.cbegin(); it != v.cend(); ++it) {
   std::cout << *it << ' ';
}
// ou encore
for (const auto &s : v) {
   std::cout << s << ' ';
}

Ici, le réflexe pour la première répétitive aurait été d'utiliser auto pour it et d'utiliser begin(v) et end(v) pour déterminer les bornes de la séquence à parcourir. Cela ferait de it, contextellement, un vector<string>::iterator ou un vector<string>::const_iterator, selon le type de v, ce qui serait adéquat.

J'ai utilisé v.cbegin() et v.cend() par souci de conformité entre les exemples, sans plus

template <class It>
   void f(It debut, It fin) {
      typename std::iterator_traits<It>::difference_type dist = std::distance(debut, fin);
      // ...
   }
template <class It>
   void f(It debut, It fin) {
      auto dist = std::distance(debut, fin);
      // ...
   }

Cet exemple est, je pense, parlant de l'utilité du mot clé auto. On peut aller vers une déclaration de type très explicite, mais l'avantage est obscur. Dans un cas comme celui-ci, je doute que la plupart des programmeuses et des programmeurs tirent véritablement profit du type tel qu'explicité (savoir s'il est signé ou non, par exemple); si quelqu'un n'est pas conscient du fait que le type de retour de distance() devrait être signé, expliciter le type ne le lui dira sans doute pas plus.

template <class M, class F, class ... Args>
   auto minuter(M minu, F f, Args && ... args) -> std::pair<decltype(f(std::forward<Args>(args)...)), typename M::duration> {
      typename M::time_point avant = minu.now();
      decltype(f(std::forward<Args>(args)...)) resultat = f(std::forward<Args>(args)...);
      typename M::time_point apres = minu.now();
      return std::make_pair(resultat, apres - avant); 
   };
template <class M, class F, class ... Args>
   auto minuter(M minu, F f, Args && ... args) {
      auto avant = minu.now();
      decltype(auto) resultat = f(std::forward<Args>(args)...);
      auto apres = minu.now();
      return std::make_pair(resultat, apres - avant); 
   };

Nous avons ici un cas où le recours à auto évite de connaître les noms des types internes des horloges standards du langage, mais où exprimer l'algorithme de manière générale ne change rien à la lisibilité de la fonction. Cela réduit aussi la répétition de code, réduisant du même coup les risques d'erreur.

Notez la subtilité de la déclaration de la variable resultat, par contre, dans l'exemple à droite. Ici, utiliser auto aurait probablement fonctionné, mais aurait entraîné un glissement sémantique... Un signe qu'il me faut pas cesser de réfléchir, même en utilisant un mécanisme tel que auto.

Le fait d'utiliser auto pour définir une variable rend illégale l'omission d'une initialisation. En effet, ceci ne compilera pas :

auto x;

... alors que ceci compilerait, mais laisserait x non-initialisée :

int x; // légal, mais...

Sur le plan esthétique, utiliser auto a comme autre mérite celui d'aligner les noms de variables dans un même bloc, tous les « noms de types » étant de la même longueur.

La position AAAA

La position AAAA est à l'effet qu'utiliser auto pour définir une variable introduit une distance entre le code source et sa sémantique, ce qui peut compliquer l'entretien et introduire des bogues suspects.

Avec auto Intention possible En pratique
auto s = "J'aime mon prof";

La programmeuse ou le programmeur souhaitait peut-être que s soit de type std::string

Ici, s est un const char*. On aurait obtenu une std::string en utilisant une construction plus explicite, par exemple :

auto s = string{"J'aime mon prof"};

... ou, depuis C++ 14, en utilisant des littéraux maison :

auto s = "J'aime mon prof"s;
auto x = d + sizeof(T);

La programmeuse ou le programmeur souhaitait peut-être déterminer la taille requise pour entreposer un T et quelque chose d'une taille de d bytes

Le type de x est un mystère : signé ou non? Entier ou non? Se peut-il que d soit négatif? Cet exemple est dû à James McNellis, qui mentionne (avec raison) qu'utiliser auto ici est une excellente manière de se mettre dans le pétrin

std::vector<std::string> v = { "J'aime", "mon", "prof" };
for (auto s : v) {
   std::cout << s << ' ';
}

La programmeuse ou le programmeur itérera bel et bien sur chaque string de v, mais créera à chaque itération une copie, potentiellement dispendieuse à construire et à détruire, de la string originale

Les écritures for(auto &s:v) ou for(const auto &s:v) régleraient le problème ici.

Avec une répétitive explicitant le fait que chaque s soit une string, indiquer string& ou const string& aurait aussi fait le travail.

On pourrait plaider que auto, qui semble magique aux yeux de certaines et de certains, obscurcit quelque peu cette consigne d'hygiène

template <class M, class F, class ... Args>
   auto minuter(M minu, F f, Args && ... args) {
      auto avant = minu.now();
      auto resultat = f(std::forward<Args>(args)...);
      auto apres = minu.now();
      return std::make_pair(resultat, apres - avant); 
   };

La valeur de retour de la fonction f(args...) sera copié dans resultat, mais ceci peut briser la sémantique attendue si cette fonction retourne une référence

Comme indiqué plus haut, decltype(auto) est la solution à la auto de C++ 14 ici, et expliciter le type de retour avec une expression decltype(...) relativement complexe faut aussi le travail.

Le fait d'utiliser auto ne doit pas nous amener à cesser de réfléchir, manifestement.

La position CRA

Ce qui suit présume que vous écrivez des fonctions courtes et dont la vocation est claire; si tel n'est pas le cas, le camp AAAA est le meilleur pour vous : votre vie de programmeuse ou de programmeur est complexe, et mieux vaut sans doute que vous soyez explicite.

Au fond, quel est l'intérêt du recours à auto?

Sur le plan de l'enseignement, auto a plusieurs vertus : indentation simplifiée des variables, consigne simple à expliciter pour des débutant, réduction des risques de variables non-initialisées, etc.

Dans la pratique, auto n'est pas magique, et s'en servir sans réfléchir peut mener à des effets de bord pervers, en particulier dans la manipulation des entiers (signe, bornes). Je me permettrai de rappeler à celles et ceux qui lisent ceci que programmer sans réfléchir, avec ou sans auto, est une idée fort discutable.

Mieux vaut utiliser auto quand :

Dans un cas comme celui ci-dessous :

template <class It>
   void f(It debut, It fin) {
      auto dist = std::distance(debut, fin);
      // ...
   }

... je ne suis pas d'avis qu'écrire auto plutôt que typename std::iterator_traits<It>::difference_type soit un geste dommageable. En effet, selon moi :

De même, imaginez la fonction suivante :

template <class C>
   void afficher_elements(const C &c, std::ostream &os) {
      for (auto &val : c)
         os << val << ' ';
   }

Ici, il est possible d'aller chercher le type exact des éléments de c de manière explicite, présumant que c soit un conteneur :

template <class C>
   void afficher_elements(const C &c, std::ostream &os) {
      for (typename C::const_iterator it = c.begin(); it != c.end(); ++it)
         os << *it << ' ';
   }

Cela dit, qu'avons-nous réellement gagné? Est-ce plus clair? Plus facile à entretenir?

Par contre, les abus existent. Écrire ceci :

auto n = 0;

... est plus long qu'expliciter le type (int) et risque de confondre les gens qui examineront le code a posteriori. De même, écrire ceci :

template <class T, class F>
   void g(T val, F f) {
      auto x = f(val);
      // ... beaucoup de trucs
   }

... peut-être périlleux. Quelle est l'intention derrière l'application de f à val? À quoi s'attend g() de x en pratique? Ici, contraindre le type de x est à la fois une forme de documentation et un guide pour l'entretien du code par la suite.

Mon utilisation personnelle d'auto

Personnellement, j'écris auto quand :

J'évite auto quand :

Lorsqu'il y a lieu d'expliciter des contraintes, nous avons plusieurs options (pas nécessairement dans l'ordre; choisir un ordre, ici, me semble être une question culturelle) :

L'approche AAA, donc toujours auto ou presque? Dans certains projets, peut-être, et ça me semble raisonnable pour l'enseignement, surtout en début de parcours.

L'approche AAAA, donc éviter auto sauf dans des cas exceptionnels? Je n'irais pas là, les cas où auto est pertinent étant trop nombreux.

Le type est clair à partir du contexte? Alors, auto est sans doute approprié.

Le type n'importe pas dans le contexte de l'algorithme? Alors, auto est sans doute approprié.

Réflexions...

Une conséquence directe de l'utilisation du mot clé auto est, à mon avis, un recours accru à des littéraux explicitement typés, incluant les littéraux maison. Je ne suis pas certain s'il s'agit d'une bonne ou d'une mauvaise idée pour le moment, mais je constate qu'il s'agit d'une divergence quant aux pratiques existantes, où nous avons tendance à laisser les constructeurs réaliser des conversions pour nous à partir de littéraux de divers types. Ce glissement était en partie observable dans la partie à travers le recours à des fonctions comme std::accumulate(), pour lesquelles le type de retour est déterminé par le type d'un de ses paramètres.

L'acceptation d'auto tient en partie de nos habitudes de programmation générique. Nous en sommes venus, au fil des années, à utiliser des types très abstraits dans nos programmes, types qui ne sont déterminés qu'au point d'utilisation, qui est souvent situé relativement loin du lieu où les templates sont eux-mêmes exprimés. Les outils que nous nous sommes donnés (en particulier, les traits) pour contraindre les types et préciser nos intentions demeurent valides, mais peuvent être lourds à manipuler. Je ne pense pas qu'auto les remplace entièrement, bien que ce mot clé permette d'alléger certains de leurs cas d'utilisation.

Je m'attends à ce que les concepts, alors qu'ils seront rendus disponibles par nos compilateurs, remplaceront auto dans certains cas. Ils ne sont pas aussi lourds à utiliser que des traits sur des types dépendants, et permettent de contraindre les types génériques juste assez pour clarifier l'intention et accroître la résilience du code.

Dans une présentation de 2015 portant sur la migration de code C++ 03 vers C++ 14, Joel Falcou examine auto du point de vue de la réduction du temps de compilation, et offre ces conseils :

Il ajoute que decltype devrait être utilisé pour fins de métaprogrammation et dans des contextes SFINAE. Ces pratiques amènent selon lui les meilleurs temps de compilation, dans la mesure où l'on demeure alertes face aux règles de déduction de types associées à auto, dont discute Scott Meyers dans Effective Modern C++.


Valid XHTML 1.0 Transitional

CSS Valide !