26 août 2016

Auto : pièges et évolutions du C++ moderne

Design & Code

Grow

Together

26 août 2016

Auto : pièges et évolutions du C++ moderne

Design & Code

Grow

Together

26 août 2016

Auto : pièges et évolutions du C++ moderne

Design & Code

Grow

Together

Un des premiers mots-clefs que les développeurs utilisent lors du passage au C++11/14/17 est auto. Cet article a pour but de couvrir les différents usages d’auto, au travers d’exemples plus ou moins complexes, combinés à d’autres ajouts du C++ moderne, ainsi que les pièges à éviter.

Historique du mot-clef auto

Le mot-clef auto existe depuis le langage C. Il avait une signification bien particulière et était rarement utilisé. Ce dernier servait à qualifier une variable à l’aide d’une portée automatique (détruite automatiquement lorsque cette variable sortait de cette portée).

Le mot-clef est très peu rencontré dans du code C, car les variables sont déjà automatiquement déjà qualifiées par la portée déterminée par auto.


Output :


Ici, la variable i n'existe que dans la portée de cette fonction.

Auto, pourquoi l'utiliser ?

Auto est désormais utilisé à la place d’un type (sans en être un), servant à inférer une expression. En d’autres termes : déduire le type d’une expression. Il peut être associé à des qualificateurs comme les références ou const.

Voici une liste de ses avantages :

  • Force l'initialisation d'une variable, évitant au développeur un oubli d'initialisation, renforçant ainsi la sécurité du code

  • Evite les conversions implicites de types

  • Renforce l'aspect sémantique, en évitant au développeur de se soucier de l'aspect syntaxique

  • Généralement un gain d'efficacité quand du code est écrit, particulièrement pour les tests unitaires ou seul l'aspect fonctionnel est intéressant

Passons maintenant à plusieurs exemples de déclaration basique :


On constate que les déclarations sont identiques à une

déclaration de forme auto nom = expression. On remarque aussi que les

qualificateurs sont bien présents (const et référence).


Maintenant, on peut constater plusieurs choses, comment obtenir un entier non signé ? un float ? une string ? Passons-les en revue :


Pour les utiliser, il faut utiliser les literals du C++14. On ne peut pas obtenir directement un type float ou unsigned sans eux. Pour les string, il faut utiliser le using namespace std ::string_literals, afin d’accéder à l’opérateur "" s.

Après ces quelques exemples de déclarations, passons aux déclarations de types complexes.

auto myInitListForVect = { 42u,3321u,1u }; // std::initializer_list<unsigned int>
auto myInitListForList = { "Hello"s,"Blue"s,"World"s }; // std::initializer_list<std::string>
auto myInitListForSet = { 33.2f,3321.f,1.f }; // std::initializer_list<float>
std::vector<unsigned int> myvector{ myInitListForVect }; // vector
std::list<std::string> mylist{ myInitListForList }; // list
std::set<float>

Ici, grâce au C++11, on obtient des initializer_list qui sont des objets proxy légers, donnant accès à un tableau d’objets de type const T et on initialise des containers génériques de la STL à l’aide de ces objets.

Passons à des cas un peu spéciaux, l’initialisation de rvalues références avec auto.


Sortie console :


Rappel : decltype sert à inspecter le type d’une entité ou d’une expression.

Que s’est-il passé ? La présence des && ne signifie-t-elle pas toujours rvalue référence ? Et bien non, pas dans ce cas ! Pour être exact, && signifie parfois rvalue référence OU lvalue. Donc parfois, il s’agit d’une simple référence.

Comment les détecter ? Voici quelques règles :

  • Si le type d'une expression est une référence Ivalue, cette expression est une Ivalue

  • Si on peut prendre l'adresse d'une expression, il s'agit une Ivalue

  • Sinon, c'est une Rvalue

En parlant de decltype, depuis C++14, il est possible d'utiliser decltype(auto). Exemple :


Decltype(auto) dans une déclaration de variable utilise les règles de déduction de type de decltype, et auto est remplacé par l’expression de son initialiseur.

Et pour une fonction ?

template<typename T>

Ici, le decltype est utilisé pour indiquer la valeur de retour de la fonction, et était nécessaire jusqu’au C++14. Il n’est maintenant plus nécessaire d’écrire cette syntaxe afin d’obtenir la valeur de retour :

template<typename T>

Rappel : constexpr est un mot clef qui indique qu’il est possible d’évaluer une expression, une fonction, une value au moment de la compilation.

Auto sert aussi dans le cadre des lambdas, quelques exemples :

auto lambdaSum = [](int a, int b) { return a + b; }; // simple lambda renvoyant une addition
auto lambdaMult = []

Sortie console :


Reprenons nos exemples de liste, vecteur et set déclarés plus haut, nous allons maintenant voir comment auto permet d’itérer facilement sur des containers et de différentes manières :

for (auto it = myInitListForSet.begin(); it != myInitListForSet.end(); ++it) // deduction du type, qui est un iterateur
	std::cout << *it << std::endl;
for (auto& listIter : myInitListForList) // iteration sur myInitListForList, en utilisant une ref pour éviter la copie
	std::cout << listIter << std::endl;
for (auto i = 0u; i != myvector.size(); ++i) // attention ! la fonction size() des vecteur renvoie un size_t,
	std::cout << myvector[i]

Sortie console :


Il reste encore quelques exemples à explorer, comme les auto dans les paramètres des fonctions templatés, depuis le C++17 , avec ce petit exemple :


Sortie console quand un appel est fait la fonction : product<32,45>()


Derniers exemples possibles, en C++17, avec auto, des déclarations de liaison structurées, commençons par un tableau :

int tableau[4] = { 5, 2, 42, 3};
auto[w, x, y, z] = tableau; // copie et initialisation de chaque variable de tableau
auto&[r, s, t, u]

Ici, on déclare un simple tableau, en l’assignant ensuite à des variables automatiques, non référence et référence. Voici la sortie :


On constate effectivement que les qualificateurs de

variables, référence par exemple, sont appliqués aux déclarations de liaison

structurées.


Il existe deux autres exemples avec des tuples ou des membres de données d’une classe :

struct myStruct
{
	int a;
	volatile double b;
};
unsigned int uinttuple{3};
char chartuple{'a'};
std::tuple<unsigned int&&, char> mytuple(std::move(uinttuple), chartuple); // déclaration d'un tuple 
const auto& [myint, mychar] = mytuple; // myint est une rvalue reference, mychar est un char
std::cout << myint << " " << mychar << std::endl;
myStruct myStructure{3, 2.5}; // assignation de quelques valeurs dans les deux champs de la structure
auto[sint, sdouble]

Et la sortie :

Apres ces séries d’exemples en tout genre, il est temps d’attaquer un petit paragraphe qui sert de conclusion.

Auto, quand l'utiliser ?

C’est ici que la plupart des développeurs C++ n’ont pas les mêmes avis. Certains préfèrent utiliser cette nouvelle utilisation avec parcimonie, typiquement dans des variables temporaires comme des itérateurs dans des boucles.

Du coté des professionnels du langage, comme Scott Meyers ou Herb Sutter, ou des entreprises comme Microsoft, ils sont plutôt avocats de l’utiliser quasi tout le temps. Que ce soit pour des valeurs de retour ou des récupérations de valeurs complexes afin de s’affranchir de la lecture technique d’une fonction.

Pour la plupart des développeurs, l’adaptation s’effectue surtout dans l’équipe selon les versions des compilateurs et de la volonté d’évolution du code existant.