logo le blog invivoo blanc

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

4 février 2019 | C++ | 2 comments

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.

void dummyfunc()
{
	auto int i = 0;  // qualificateur de portée

	printf("%d\n", i);
}

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 :

auto integer = 0; // int
auto dbl = 33.5; // double
auto copy = integer; // copie de integer
auto& ref = integer; // int& sur integer
const auto constInt = 0; // const int
const auto& refConstInt = constInt; // const int ref sur constInt
auto testNullptr = nullptr; // nullptr_t c++11
auto testOldNull = NULL; // null
auto str1 = "i'm str1 sentence"; // const char*

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 :

using namespace std::string_literals;

auto floa = 12.3f; // float
auto intergeru = 3u; // unsigned int
auto myString = "this is a string"s; // std::string

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> myset{ myInitListForSet }; // set

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.

auto i = 0; // int
auto&& rvf = i; // pas une rvalue reference !
auto&& trvf = std::move(i); // la, c'est une rvalue reference !

	// vérification :
if (std::is_same<decltype(rvf), int&&>::value)
	std::cout << "rvf is &&" << std::endl;
if (std::is_same<decltype(rvf), int&>::value)
	std::cout << "rvf is &" << std::endl;
if (std::is_same<decltype(trvf), int&&>::value)
	std::cout << "trvf is &&" << std::endl;
if (std::is_same<decltype(trvf), int&>::value)
	std::cout << "trvf is &" << std::endl;

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 :

auto integ = 4 + 5;
decltype(auto) copyInt = integ; // copie d'integ
decltype(auto) refInt = (integ); // référence sur integ !

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>
inline constexpr auto sum(T a, T b) -> decltype(a + b)
{
	return a + b;
}

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>
inline constexpr auto sum(T a, T b) noexcept
{
	return a + b;
}

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 = [](auto n, auto m) { return n * m; } ; // depuis le c++17, les lambdas acceptent des paramètres auto !

std::cout << "lambdaSum = " << lambdaSum(-4, 3) << std::endl;
std::cout << "lambdaMult = " << lambdaMult(3, 2.5) << std::endl;

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] << std::endl;   // qui est un define sur unsigned int, ne pas oublier le literal

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 :

template<auto T, auto U>
inline constexpr auto product()
{
	return T * U;
}

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] = tableau; // reference du tableau pour chaque alias

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 :

std::cout << w << " " << x << " " << y << " " << z << std::endl; // affichage des valeurs

std::cout << &r << " " << &s << " " << &t << " " << &u << std::endl; // 

std::cout << &tableau << std::endl; // l'adresse du tableau est identique à l'adresse de la référence r

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] = myStructure; // ça fonctionne ! Attention cependant, l'assignation ne marchera pas si les membres ne sont pas accessibles

std::cout << sint << " " << sdouble << std::endl;

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.