Cyag

Yohann Agrebbe

Programmeur Unix/C++ en freelance

Blog

Pourquoi le type « auto » en C++ devrait être banni

(modifié 2 fois)

Ces dernières années, parmis les experts de renommée mondiale, on trouve quelques défenseurs de l'utilisation systématique du mot clé « auto » pour déclarer toute variable dans un code, recommandant vivement l'adoption du principe de l'AA (Always Auto), ou a minima, de l'AAA (Almost Always Auto). En revanche l'idée opposée, qui consisterait à rejetter intégralement cette pratique, est quasi inexistante dans la grande littérature du C++. Serait-ce donc le signe d'une écrasante victoire argumentative de la part des auto-enthousiastes ?

Rien n'est moins sûr, il arrive parfois que des points de vue soient peu défendus car ils concordent avec les pratiques déjà massivement répandues. Il n'est nul besoin de militer pour un monde qui est déjà en place, et dont les bases sont suffisamment solides pour résister à ceux qui souhaitent le renverser.

Toutefois, depuis peu et dans tout type de domaine, on observe une tendance des théories farfelues à sortir de l'ombre pour conquérir l'opinion publique, avec parfois quelques malheureuses répercussions politiques. Le C++ a beau être un sujet dérisoire comparé aux grands problèmes de l'humanité, il faut bien que quelqu'un cherche à le préserver lui aussi de la bêtise et de la « hype disruptive », notamment dans un contexte où les grands décideurs de l'avenir du langage, pleinement occupés par l'animation de conventions et la production de best-sellers, n'ont plus travaillé sur du vrai code depuis des décennies. Il serait donc bon de rappeler de temps en temps ce qui fait la force du C++, et d'insister sur le fait qu'il existe d'autres langages très efficaces comme le python pour les développeurs amoureux de l'abstraction matérielle et de la conception faiblement typée.

Le cast en tant qu'instruction

À l'instar du C, le C++ est fait pour apporter au programmeur une très grande maîtrise de l'aspect technique de son travail. Le programmeur sait quand une variable est stockée sur la pile, il sait quand elle est allouée sur le tas, il peut contrôler lui-même la libération de la mémoire et l'affectation des pointeurs. Il peut par ailleurs jouer sur les formats d'entiers, selon qu'ils soient signés ou non, puis exploiter l'effet des limites selon leur taille pour établir une arithmétique modulaire. C'est le cas par exemple d'un générateur congruentiel linéaire, qui nécessite de réaliser des dépassements de maximum avec une connaissance du nombre de bits exacts de chaque variable :

uint32_t random_delphi(uint32_t range, uint32_t &seed) {

	uint64_t mult, L = range;

	seed = seed*134775813 + 1;
	mult = seed;

	return (L*mult) >> 32;

}

Cette fonction risquerait de donner des résultats aberrants avec des types automatiques, puisque la précision du type fait partie du calcul.

Autre exemple plus typique du quotidien des programmeurs en C++, la lecture d'un entier depuis un fichier binaire. Il est extrêmement important de définir avec exactitude le type des variables pour garantir le bon fonctionnement du code suivant :

int16_t read_signed_word(std::ifstream &ifs, bool big_endian) {

	uint8_t c;

	int16_t low, high;
	int16_t *first, *second;

	if (big_endian) {
		first = &high;
		second = &low;
	}
	else {
		first = &low;
		second = &high;
	}

	c = ifs.get();
	*first = c;

	c = ifs.get();
	*second = c;

	return (high << 8) | low;

}

Utiliser d'autres types que ceux précisés pourrait provoquer des troncatures de valeur ou des erreurs de signe.

La sécurisation par le typage

Mais outre les besoins en technicité, il est aussi question de concept : Demander au programmeur de s'assurer à tout moment qu'il utilise le bon type est un énorme point fort du C++, cela permet d'interrompre la compilation, c'est à dire d'empêcher la réalisation du produit, tant que le code n'est pas parfaitement typé. Et c'est une bonne chose, car un type qui est pris pour un autre par inadvertance est une source de bugs assurée.

D'autres langages comme le PHP sont dépourvus de cette rigueur et sont faits pour déclarer des variables sans type. En conséquence, les développeurs s'affranchissent de l'étude de certaines problématiques, et l'absence de compilation laisse passer des erreurs d'inattention telles que :

function print_nerds() {

	$NAME_INDEX = 0;
	$EURO_INDEX = 1;

	$nerds = array();

	$nerds[] = array($NAME_INDEX => "Alice", $EURO_INDEX => 343);
	$nerds[] = array($NAME_INDEX => "Bob",   $EURO_INDEX => 256);

	echo '<h1>Liste des consultants</h1>';
	echo '<ul>';

	foreach ($nerds as &$nerd) {

		$name = $nerd[$NAME_INDEX];
		$cost = $nerd[$NAME_INDEX]; // bug

		echo '<li>Nom&nbsp;: "'.$name.'", tarif&nbsp;: '.$cost.'</li>';

	}

	echo '</ul>';

	return count($nerds);

}

Dans ce code PHP, la récupération du coût du consultant aurait dû se faire avec l'indice « $EURO_INDEX », mais l'absence d'exigence de typage a permis d'utiliser « $NAME_INDEX » à la place. Il y a donc un risque que le bug soit découvert tardivement, et par l'utilisateur final de l'application. Selon l'implémentation qui en est faite, le même code en C++ peut provoquer un échec de compilation, l'erreur serait donc détectée en amont et la mise en production ne pourrait pas avoir lieu sans qu'elle soit corrigée :

size_t print_nerds() {

	std::vector<std::pair<const char*, int>> nerds;

	nerds.push_back(std::make_pair("Alice", 343));
	nerds.push_back(std::make_pair("Bob", 256));

	std::cout << "Liste des consultants :" << std::endl;

	for (std::vector<std::pair<const char*, int>>::iterator nerd = nerds.begin() ; nerd != nerds.end() ; ++nerd) {

		const char* name = nerd->first;
		int cost = nerd->first; // error

		std::cout << "- Nom : \"" << name << "\", tarif : " << cost << std::endl;

	}

	return nerds.size();

}

Avec les indications du compilateur, le programmeur comprendra immédiatement qu'il doit remplacer « nerd->first » par « nerd->second » pour récupérer correctement le tarif du consultant. En revanche, l'utilisation radicale du mot clé auto aurait fait perdre ce bénéfice du typage, et aurait laissé passer l'erreur tout comme en PHP :

auto print_nerds() {

	std::vector<std::pair<const char*, int>> nerds;

	nerds.push_back(std::make_pair("Alice", 343));
	nerds.push_back(std::make_pair("Bob", 256));

	std::cout << "Liste des consultants :" << std::endl;

	for (auto nerd = nerds.begin() ; nerd != nerds.end() ; ++nerd) {

		auto name = nerd->first;
		auto cost = nerd->first; // bug

		std::cout << "- Nom : \"" << name << "\", tarif : " << cost << std::endl;

	}

	return nerds.size();

}

En fait, cet exemple n'est pas si réaliste dans la mesure où les programmeurs auront plutôt tendance à envoyer directement dans « std::cout » les valeurs à afficher, sans passer par des variables intermédiaires. Ceci rendrait alors le bug possible même sans utiliser un type auto, puisque la surcharge de fonction est une capacité historique du C++. Mais comme on le dit chez personne, ce n'est pas parce qu'on peut se blesser avec une machette (surchargée) qu'il faut découper le gigot d'agneau à la tronçonneuse auto, mieux vaut éviter tout risque et utiliser un bon vieux couteau de cuisine sur mesure, prévu pour la tâche qui lui est confiée. On pourrait alors imaginer une philosophie du NANO (Neither Auto Nor Overloading) qui consisterait à accorder toute son attention au typage : Tout comme en langage C, il y aurait une fonction différente par type de paramètre, comme un « print_string() » pour afficher une chaîne et un « print_integer() » pour afficher un entier. Ceci permettrait de pallier aux confusions induites par le principe des surcharges.

La vértiable nature du C++

Cependant, il faut bien placer le curseur quelque part, car à trop élaguer les branches du C++ on risque de se retrouver avec un C tout brut. Le fait de tolérer les surcharges était déjà une chose permissive, la poire était coupée en deux entre d'un côté la rigueur fastidieuse du C, et de l'autre le laxisme insouciant de la plupart des autres langages. Le C++ faisait figure de dosage optimal entre les différentes philosophies qui s'affrontent, et a toujours su remettre à leur place les aigris aux mains pleines de cambouis lorsqu'ils venaient fustiger les spécificités de la programmation objet, tout comme il savait mettre à la porte les VRP de l'ignorance technique cherchant à vendre leurs utopies. Chacun aura le loisir d'illustrer ces descriptions par les collègues de son choix, car tout le monde a forcément eu affaire à au moins un exemplaire de ces deux catégories de personnes.

Mais avec l'arrivée de ce mot clé auto, ainsi que de quelques autres nouveautés des versions 11+, d'aucuns ont cru que le C++ cherchait à évoluer vers un ersatz de PHP ou de javascript, ils expliqueront alors au monde entier qu'un bon code de nos jours est un code qui n'utilise plus les pointeurs, qu'il ne doit pas non plus dépasser les trois niveaux d'indentation, et que le goto est un crime contre l'univers. En résumé, toutes les caractéristiques historiques du langage seront présentées comme étant obsolètes, et c'est dans ce contexte d'incompréhension ou d'oubli du véritable C++ que l'initiative du « Always Auto » survient.

Il faut dire aussi que bien comprendre et maîtriser le C++ ne vient pas en un simple claquement de doigts, et cela doit rigourseuement s'entretenir une fois acquis. Une expression à rallonge telle que « std::vector<std::pair<const char*, int>>::iterator nerd = nerds.begin() » est particulièrement hostile aux yeux d'un débutant, surtout à une époque où les interfaces graphiques du quotidien ont la fâcheuse volonté de déshabituer les cerveaux humains à la profusion textuelle. Mais un véritable programmeur maîtrise les concepts de la STL et n'est aucunement ralenti par cette ligne de code, elle lui parle comme il lirait une phrase dans sa langue maternelle. Du coup, le fait même d'invoquer l'argument de la lisiblité personnelle pour utiliser un type auto dans ce cas de figure risque de passer pour un aveu d'incompétence aux yeux des plus fervents opposés à l'AA·A, en leur rappelant les réticences des enfants en classe de CP lorsqu'ils doivent lire un mot d'une vingtaine de lettres, tandis que les adultes peuvent lire efficacement une phrase parsemée de mots complexes sans même prêter attention à sa longueur ou à sa structure.

Oui, tout comme l'apprentissage d'une langue, d'une écriture, ou du solfège, maîtriser le C++ nécessite un remaniement cérébral ! Grands experts mondiaux en théorie généraliste sur le développement informatique ou simples geeks bidouilleurs touche-à-tout, le ticket d'entrée est le même pour tout le monde, à défaut d'accomplir les 25 milliers d'heures de pratique nécessaires pour atteindre un niveau remarquable en C++ il est sage de se tourner vers les autres langages, qui correspondent davantage aux intuitions personnelles ou qui sont conçus exprès pour fonctionner rapidement entre toutes les mains, et cela vaut mieux que d'établir une approche révolutionnaire dévastant le C++ à grand coup d'homogénéisation.

Le C++ en tant que neveu d'Unix

Par ailleurs, l'aspect et l'origine très proche du C que l'on retrouve dans le C++ le rendent très naturellement conciliable avec le monde Unix. Or dans celui-ci, il existe des outils très performants pour réaliser diverses opérations sur un gros volume de fichiers texte, tels que « grep » et « sed ». Ainsi, un programmeur sera bien plus performant s'il maîtrise le Shell Unix, il pourra rechercher efficacement des bribes de code et réaliser des modifications de masse. Mais le jour où un code donne le type auto à toutes ses variables, il devient beaucoup plus difficile de l'analyser.

Par exemple, la ligne « size_t max = print_nerds(); » donnerait une bonne idée de ce que peut contenir la variable « max », tandis que la ligne « auto max = print_nerds(); » pourrait semer le doute, sachant que le prototype de la fonction serait certainement « auto print_nerds() », ce qui n'arrangerait pas la recherche. Et si dans ce cas précis un nom de variable correct pourrait lever l'ambiguïté, ça ne serait pas toujours le cas d'un exemple plus complexe avec des classes, car on a toujours besoin de savoir quelle classe on utilise, de savoir quelles sont les méthodes potentiellement appelées et quels mécanismes implicites se cachent derrière le code. Ceux qui prétendent l'inverse en disant qu'une analyse est possible sans entrer dans les détails techniques, que le code peut s'affranchir tout aspect matériel pour se concentrer uniquement sur la conception générale sans que cela ne porte préjudice au processus de développement, n'ont de toute évidence jamais débuggé un programme en C++, une opération qui nécessite parfois d'aller jusqu'à consulter le code assembleur généré par la compilation.

C'est là aussi que la différence entre la surcharge de fonction et le type automatique se fait ressentir, avoir affaire à une fonction surchargée ne demande pas de remonter le fil du courant lors d'une analyse, car la commande la plus évidente pour trouver une déclaration de prototype permet aussi d'en visualiser toutes les surcharges :

grep -r 'print_value' --include '*.h'
Un dosage optimal entre abstraction et bas-niveau

Ainsi selon toutes ces considérations, le constat est sans appel : Les grands théoriciens éloignés des réalités techniques du terrain, qui suggèrent l'adoption du AA ou du AAA, ne sont pas différents des experts en C et assembleur qui refusent de créer des classes ou d'utiliser l'expression « const » quand ils doivent toucher à un code en C++, chacun veut borner les usages de tous à ce qu'il connaît déjà, inspectant et jugeant les technologies via le cribble de son domaine d'expertise. Parce que l'humain aime transformer le monde sans quitter sa zone de confort, il demande au C++ de devenir un langage qu'il n'est pas, en le tirant vers soi plutôt que de faire l'effort de saisir sa qualité inestimable de point d'intersection entre la puissance conceptualisatrice de l'orienté objet et la performance d'une stricte gestion des ressources logicielles.

Pour cette raison, agissons tant qu'il n'est pas trop tard, bannissons les auto pour dépolluer le code.