Évolution du mot clef auto

Sommaire

Le mot clef auto, ses avantages et ses différents comportements selon la version et le contexte.

Automatic storage duration specifier (avant C++11) (obsolète)

En langage C, auto (pour “Automatic Storage Duration”) sert à spécifier qu’une variable a une portée locale à son bloc de code (scope).

C’est à dire, faire que la variable soit supprimée à la sortie du scope, contrairement aux variables globales ou statiques.

void print42()
{
	auto int number = 42;
	printf("%d\n", number);
}
// La variable number n'existe pas en dehors de la fonction print42()

Dans les premières versions du langage C (Le “C K&R”), il était obligatoire de déclarer explicitement les variables locales avec auto.

Dès l’arrivée du C ANSI (C89) en 1989, le comité de standardisation créé pour l’occasion décide de rendre les variables locales auto par défaut et le mot clef auto optionnel, le rendant par conséquent redondant et inutile à renseigner explicitement.
Il reste cependant supporté dans les versions suivantes du C pour des raisons de rétrocompatibilité.

En C++, auto avait la même signification jusqu’au C++11.
Mais cette utilisation du mot clef auto était déjà largement obsolète bien avant l’introduction du C++11.

A partir de C++11, le mot clef auto se voit attribuer une autre signification pour faire de l’inférence de types.

Placeholder type specifiers (depuis C++11)

Dès C++11, le mot clef auto permet de faire de l’inférence de types.
En écrivant auto à la place du type d’une variable, le type de la variable est déduit à partir de la valeur à droite du signe égal.

auto a = 1; // int
auto b = 2u; // unsigned int
auto c = "foo"; // const char*
auto d = std::string{"bar"}; // std::string
auto e = std::size(d); // std::size_t
auto f = 3uz; // std::size_t
auto g = { 1, 2, 3 }; // std::initializer_list<int>
auto h = std::array{ 1, 2, 3 }; // std::array<int, 3>
auto i = nullptr; // std::nullptr_t

Les literals facilitent le typage lors de l’initialisation de variables.

Cette déduction de type est faite à la compilation (typage statique) et ne permet pas de faire du typage dynamique.
Contrairement à var/let en JavaScript, le mot clef auto ne permet pas à une variable de changer de type en cours de route. Son type reste fixe.

Par défaut, auto ne récupère pas les propriétés cvref (const/volatile/reference) de la valeur qui lui est assignée. Il faut penser à bien les renseigner pour éviter les copies inutiles.

const std::string& Object::get_name() const;

auto string0 = object.get_name(); // Prend une copie
const auto& string1 = object.get_name(); // Prend une référence constante

Le * des raw-pointers est bien déduit par le mot clef auto, mais il est préférable de l’expliciter en écrivant auto*.

auto string = std::string{"Hello World"};
auto c_string0 = std::data(string); // c_string0 est de type char*
auto* c_string1 = std::data(string); // c_string1 est de type char*

L’usage explicite de auto* permet de signaler de manière claire que vous travaillez avec des pointeurs, ce qui peut améliorer la lisibilité du code.

A noter que l’écriture auto string = std::string{"Hello World"}; est appelée “auto to track”.
Elle consiste à forcer la variable string à adopter le type à droite du signe égal (std::string).

L’écriture auto c_string0 = std::data(string); est quant à elle nommée “auto to stick”.
Elle consiste à déduire le type de la variable c_string0 en fonction du type retourné par la fonction std::data.

Common type deduction

Lorsqu’un type dépend de plusieurs expressions, l’utilisation de auto permet au compilateur de déduire le type commun entre les différentes expressions possibles.

Par exemple, dans le cas d’une ternaire où c peut se voir attribuer la valeur de a ou de b selon une condition:

auto c = (a < b) ? a : b;

Si a et b sont de types différents, le mot clef auto permet de déduire automatiquement le type commun de ces deux expressions.

auto a = 10; // int
auto b = 3.14; // double

auto c = (a < b) ? a : b; // Type commun entre int et double (double)

Équivaut à:

auto a = 10; // int
auto b = 3.14; // double

std::common_type_t<int, double> c = (a < b) ? a : b; // double

Ici, le type commun de int et double est le type double, car un double peut être construit à partir d’un int mais l’inverse n’est pas possible directement.

double a = 10; // int vers double: Ok
int b = 3.14; // double vers int: Erreur
<source>:9:27: error: implicit conversion from 'double' to 'int' changes value from 3.14 to 3 [-Werror,-Wliteral-conversion]
    9 |         int b = 3.14;

Typer une lambda

auto permet également de typer une lambda.
En effet, en C++ chaque lambda a un type unique qui lui est propre, et ce, même si plusieurs lambdas ont la même signature.
Ecrire explicitement leur type est donc impossible. L’utilisation du mot clef auto est le seul moyen de typer une variable contenant une lambda:

auto sum = [](int lhs, int rhs) -> int { return lhs + rhs; };

Attention, le mot clef auto est différent pour les paramètres de fonctions. On aborde ce point plus bas.

Trailing return type (depuis C++11)

En C++, le type de retour des fonctions est écrit au début de leur définition/déclaration:

int sum(int lhs, int rhs)
{
	return lhs + rhs;
}

Dans d’autres langages, le type est la plupart du temps spécifié à la fin de leur définition/déclaration:

En Python:

def sum(lhs: int, rhs: int) -> int
	return lhs + rhs

En Typescript:

function sum(lhs: number, rhs: number) : number
{
	return lhs + rhs;
}

Pour revenir au C++, le trailing return type permet de spécifier le type de retour des fonctions à la fin de leur définition/déclaration (depuis C++11):

auto sum(int lhs, int rhs) -> int
{
	return lhs + rhs;
}

Cette écriture permet entre autre de définir un type de retour qui dépend des paramètres de la fonction.

template<class Lhs, class Rhs>
auto sum(Lhs lhs, Rhs rhs) -> decltype(lhs + rhs)
{
	return lhs + rhs;
};

Si vous n’êtes pas familiers avec les templates, passez faire un tour ici. Et pour decltype(expression), c’est ici.

Cela n’est pas possible avec l’ancienne écriture des fonctions:

template<class Lhs, class Rhs>
decltype(lhs + rhs) sum(Lhs lhs, Rhs rhs)
{
	return lhs + rhs;
};

<source>:5:10: error: ‘lhs’ was not declared in this scope;
<source>:5:16: error: ‘rhs’ was not declared in this scope;

Le compilateur comprend les déclarations dans l’ordre dans lequel il les lit. Et comme il lit les fichiers de haut en bas et de gauche à droite, il ne connait pas encore lhs et rhs à l’instant où on les utilise dans decltype(lhs + rhs).

Cette nouvelle syntaxe apporte aussi une uniformisation entre la syntaxe des fonctions et celle des lambdas.

Les lambdas (C++11) s’écrivent de la façon suivante, avec le type de retour à droite:

auto sum = [](int lhs, int rhs) -> int {
	return lhs + rhs;
};

A noter qu’ici, auto n’est pas le type de la valeur de retour de la lambda, mais le type de la lambda elle-même.
Ca a été abordée dans la section précédente.

En résumé, utiliser auto avec le trailing return type permet d’uniformiser la manière dont les types de retour sont déclarés et assure une meilleure lisibilité, surtout dans les fonctions dont le type de retour dépend des paramètres.
Cette pratique est recommandée en C++ moderne.

AAA (Almost Always Auto) (depuis C++11)

Le principe AAA (Almost Always Auto) a vu le jour dès le C++11 pour encourager l’utilisation d’auto par défaut.

Comme nous venons de le voir, auto apporte de nombreux avantages, aussi bien pour la lisibilité et l’apport de nouvelles fonctionnalités.

Quelques avantages notables à utiliser auto :

  • Force l’initialisation des variables, évitant au développeur un oubli d’initialisation (int i;), évitant ainsi des erreurs
  • Évite les conversions implicites lors de l’initialisation des variables (float f = 1;: conversion implicite de int vers float)
  • Plus agréable à écrire pour les types longs (Par exemple les itérateurs)
  • Couplé à l’initialisation uniforme, il contribue à réduire la charge mentale causée par les multiples façons d’écrire la même chose. Le C++ devient un langage beaucoup plus lisible et abordable.
  • Le type est déjà renseigné (ou déduit) à droite du signe égal, pas de redondance en l’écrivant aussi à gauche.
  • Les templates deviennent beaucoup plus lisibles
  • auto est le seul moyen de typer une lambda

Mais il reste un problème:
Dans l’écriture suivante, le compilateur n’est pas tenu de considérer la ligne comme étant une simple initialisation de variable:

auto string = std::string{"Hello World"};

Le compilateur peut considérer cette instruction comme étant la création d’une valeur, puis son déplacement dans la variable string. Engendrant un léger surcoût.

Il ne faut cependant pas négliger les capacités d’optimisation des compilateurs, qui la plupart du temps parviennent à supprimer le coût de ces déplacements.

Ce surcoût est généralement considéré comme négligeable, sauf dans certains cas où l’opération est coûteuse:

auto array = std::array{1, 2, 3, 4, 5};

std::array étant un type trivial, son déplacement fait une copie, représentant là aussi un surcoût.
Ici aussi, on peut décider de ne pas utiliser auto pour éviter ce surcoût.

Dans certains cas, l’écriture avec auto est même impossible. Lorsqu’un type est non-copyable ET non-movable:

auto m = std::mutex{}; // Ne compile pas en C++14
std::mutex m{};
auto lock = std::lock_guard<std::mutex>{m}; // Ne compile pas en C++14
std::mutex m{};
std::lock_guard<std::mutex> lock{m}; // Compile

Ceci explique le “Almost” dans “Almost Always Auto”. On est passé à ça 🤏 d’avoir une règle d’écriture uniforme.

Certains développeurs préfèrent utiliser auto avec parcimonie, en remplacement de types particulièrement verbeux (notamment les iterateurs). D’autres prônent son utilisation quasi systématique, comme Scott Meyers et Herb Sutter.

Certains seraient même tentés de ne jamais utiliser auto pour éviter ce genre de problème, et passer à côté de tous les autres avantages qu’il apporte.

Mais ne vous arrêtez pas au “Almost Always Auto”, nous allons revenir sur ce point par la suite.

auto as a return type (depuis C++14)

A partir de C++14, on peut laisser le compilateur déduire le type de retour d’une fonction à partir des return qui la composent:

template<class Lhs, class Rhs>
auto sum(Lhs lhs, Rhs rhs)
{
	return lhs + rhs;
};

Cependant, ce n’est pas une écriture que vous verrez souvent car elle comporte des risques et qu’elle ne couvre pas toutes les situations.

Retourner auto peut être suffisant dans les définitions car le compilateur a accès aux return pour déduire le type à retourner.
Mais pas dans les déclarations car le compilateur n’a pas accès au corps de la fonction pour déduire le type de retour (par exemple lorsqu’on importe les headers d’une bibliothèque sans en avoir les sources).

Dans l’exemple suivant, un auto as a return type est utilisé dans Sum.cpp, mais pas dans Sum.h.
Dans Sum.h on utilise un Trailing return type pour renseigner explicitement le type de retour.

Sum.h

template<class Lhs, class Rhs>
auto sum(Lhs lhs, Rhs rhs) -> decltype(lhs + rhs);

Sum.cpp

template<class Lhs, class Rhs>
auto sum(Lhs lhs, Rhs rhs)
{
	return lhs + rhs;
};

Il arrive que la fonction contienne plusieurs return.

Contrairement aux ternaires, sur lesquelles auto déduit automatiquement le type commun, auto comme type de retour exige que toutes les valeurs retournées partagent exactement le même type.

auto getText(int value)
{
	if (value >= 0)
		return "La valeur est positive"; // const char*
	else
		return std::string_view{"La valeur est négative"}; // std::string_view
};

Ceci provoque une erreur de compilation, bien qu’un type commun existe (std::string_view)

<source>:9:29: error: inconsistent deduction for auto return type: ‘const char*’ and then ‘std::basic_string_view'

L’ambiguïté peut être résolue en précisant explicitement le type:

auto getText(int value) -> std::string_view
{
	if (value >= 0)
		return "La valeur est positive"; // const char*
	else
		return std::string_view{"La valeur est négative"}; // std::string_view
};

Le compilateur tente maintenant de construire un std::string_view à partir du const char* retourné. Ce qui est fait via un appel implicite à un constructeur de std::string_view.

Attention toutefois: Evitez les conversions implicites autant que possible, c’est une mauvaise pratique. Le code précédemment n’est là qu’a des fins de démonstration pour montrer les problèmes que l’on peut rencontrer avec le auto as a return type

Notez qu’il est possible de retourner auto en suivant le trailing return type pour respecter l’uniformisation:

template<class Lhs, class Rhs>
auto sum(Lhs lhs, Rhs rhs) -> auto
{
	return lhs + rhs;
};

Ici, il n’y a pas de redondance du mot clef auto.
Seul celui à droite désigne le type de retour de la fonction.
Celui de gauche est simplement nécessaire pour l’écriture du trailing return type.

Ici, il n’y a aucun intérêt autre que l’uniformisation d’écrire -> auto.
Ecrire simplement auto sum(Lhs lhs, Rhs rhs) revient au même.

decltype(auto) (depuis C++14)

Contrairement à auto, decltype(auto) permet de préserver les propriétés cvref (const/volatile/reference) d’une expression.

decltype(auto) est particulièrement utile lorsqu’il est nécessaire de préserver la nature exacte de l’expression retournée, que ce soit une référence ou un type constant:

1
2
3
4
5
6
7
8
9
10
11
int foo();
int& bar();

template<class Function>
auto call(Function function) -> decltype(auto)
{
	return function();
};

// call(foo) retourne un int
// call(bar) retourne un int&

decltype(auto) est aussi utilisable pour initialiser une variable en conservant les propriétés cvref de la valeur assignée:

auto main() -> int
{
	decltype(auto) result = call(bar);
	return result;
};

Avec cette initialisation de variable, il est possible de faire ceci:

auto i = 10; // int
decltype(auto) j = i; // int
decltype(auto) k = (i); // int&

L’utilisation de parenthèses autour de i force la déduction en référence.
Sans les parenthèses, le résultat est une copie.

Structured binding declaration (depuis C++17)

Les structured binding declaration (proposal) permettent de décomposer des objets en plusieurs variables individuelles.

Cette fonctionnalité est compatible avec:

  • Les C-like array (tableaux de taille fixe)
  • Les tuple-like (std::array, std::tuple, std::pair)
  • Les classes/structures ayant toutes leurs variables membres publiques

C-like array

1
2
3
4
5
6
7
8
9
auto main() -> int
{
	int position[2];
	position[0] = 10;
	position[1] = 15;
	auto [x, y] = position;
	
	std::print("{} {}\n", x, y); // Affiche "10 15"
}

Notez que les C-like array sont à éviter en C++. Préférez l’utilisation de std::array.

std::array

1
2
3
4
5
6
7
auto main() -> int
{
	auto position = std::array<int>{10, 15};
	auto [x, y] = position;
	
	std::print("{} {}\n", x, y); // Affiche "10 15"
}

std::tuple

using namespace std::literals;
auto pair = std::tuple{1, 2.2, "text"sv};
auto [integer, decimal, string] = pair;
std::print("{} {} {}", integer, decimal, string);

Ce n’est pas parce qu’il y a écrit auto devant une structured binding declaration que les variables partagent le même type. Chaque variable peut avoir un type différent.
Ici, auto ne désigne pas le type des variables déstructurées.

std::pair

auto pair = std::pair{1, 2};
auto [x, y] = pair;
std::print("{} {}", x, y); // Affiche "1 2"

Grace à std::pair il est possible d’obtenir les clefs et valeurs dans une range-based for loop sur une std::map/std::unordered_map.

using namespace std::literals;
auto map = std::unordered_map{
	std::pair{ "key1"sv, "value1"sv }
};
for (const auto& [key, value] : map)
	std::print("{} {}", key, value); // Affiche "key1 value1"

Cette utilisation au sein d’un range-based for loop, pour séparer clef et valeur, est l’un des principaux cas d’utilisation des structured binding declaration.

Classes/Structures

Les classes/structures ayant toutes leurs variables membres publiques sont déstructurables avec une structured binding declaration:

1
2
3
4
5
6
7
8
9
10
11
12
struct Position2d
{
	int x;
	int y;
};

auto main() -> int
{
	auto position = Position2d{10, 15}; // Construction d'un Position2d avec x vallant 10 et y vallant 15
	auto [x, y] = position; // Extraction des variables membre de Position2d
	std::print("{} {}\n", x, y); // Affiche: "10 15"
}

La déstructuration doit respecter l’ordre des paramètres.
Leur nom n’a pas d’importance, il peut être changé. Par exemple: auto [foo, bar] = position;

1
2
3
4
5
6
7
8
9
10
11
12
struct Position2d
{
	int x;
	int y;
};

auto main() -> int
{
	auto position = Position2d{10, 15};
	auto [a, b] = position;
	std::print("{} {}\n", a, b); // Affiche: "10 15" malgré l'utilisation de noms de variables différents
}

Le nombre de variables issues de la décomposition doit être strictement égal au nombre de valeurs déstructurables. Et ce, quelque soit le type du conteneur.
Ceci est également valable pour chaque type cité ci-dessous

auto position = Position2d{10, 15};
auto [x] = position; // error: type 'Position2d' decomposes into 2 elements, but only 1 name was provided
auto [x, y] = position; // Ok
auto [x, y, z] = position; // error: type 'Position2d' decomposes into 2 elements, but 3 names were provided

Propriétés cvref

Les structured binding declarations supportent les propriétés cvref, permettant d’éviter des copies inutiles ou de modifier les données contenues dans le conteneur:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct Person
{
	std::string name;
	unsigned int age;
};

auto main() -> int
{
	auto person = Person{"John Smith", 42};

	{
		auto& [name, age] = person; // name et age sont récupérés par références non-constantes
		++age;
	}
	
	{
		const auto& [name, age] = person; // name et age sont récupérés par références constantes
		std::print("{} a {} ans\n", name, age); // Affiche "John Smith a 43 ans"
	}
}

Pour mieux comprendre ces mécanismes, regardons comment ça fonctionne sous le capot.

Sous le capot

En C++, auto fait partie des éléments du langage qui ne sont que du sucre syntaxique, c’est à dire une écriture concise qui se déploie en un code plus complexe et verbeux.
C’est à la compilation que le compilateur va “remplacer” les auto par un code plus verbeux.

Pour les cas d’usage simples, auto est simplement “remplacé” par le type déduit:

auto i = 42;
int i = 42; // Résolution du type auto à la compilation

En réalité, dans cet exemple simple on dit que le type est inféré. Ici il ne s’agit pas réellement d’un remplacement de code, mais ça abouti au même résultat.

Pour les cas un peu plus complexes comme les structured binding declaration, auto est remplacé par un code légèrement plus complexe:

int array[2] = {1, 2};
auto [x, y] = array;

Equivalent produit par le compilateur:

int array[2] = {1, 2};
int __array7[2] = {array[0], array[1]};
int & x = __array7[0];
int & y = __array7[1];

Ici, __array7 est une variable créée par le compilateur à des fins de décomposition, elle aurait pu avoir n’importe quel nom tant qu’elle commence par __.
Les noms commençant par __ sont strictement réservés aux besoins internes du compilateur, pour ce genre de cas.

C’est le type de cette variable __array7 qu’on a défini en écrivant auto devant la structured binding declaration.

int i = 1;
int j = 2;
auto& [x, y] = std::make_tuple<int&, int&>(i, j); // error: non-const lvalue reference to type 'tuple<...>' cannot bind to a temporary of type 'tuple<...>'

std::make_tuple retourne un objet temporaire, qui ne peut pas être affecté à une lvalue reference non constante.

Autre exemple avec const auto&:

int array[2] = {1, 2};
const auto& [x, y] = array;

Equivalent produit par le compilateur:

int array[2] = {1, 2};
const int (&__array7)[2] = array;
const int & x = __array7[0];
const int & y = __array7[1];

Les propriétés cvref sont appliquées à __array7 et répercutées sur x et y.

Testons maintenant avec un tuple-like:

auto p = std::pair{1, 2};
auto [x, y] = p;

Equivalent produit par le compilateur:

std::pair<int, int> p = std::pair<int, int>{1, 2};
std::pair<int, int> __p7 = std::pair<int, int>(p);
int && x = std::get<0UL>(static_cast<std::pair<int, int> &&>(__p7));
int && y = std::get<1UL>(static_cast<std::pair<int, int> &&>(__p7));

On remarque que lorsqu’on utilise une déstructuration sur un tuple-like, le compilateur transforme implicitement le code en appels à std::get.

Pour les classes/structures n’ayant que des variables membres publiques, la déstructuration n’appelle pas std::get. Le compilateur génère un accès direct aux membres dans l’ordre de leur déclaration.

Vous pouvez faire vos propres analyses de transpilation de codes C++ sur l’outil en ligne CppInsights.

Variables membres privées

Pour rappel, les types compatibles avec les structured binding declaration sont:

  • Les C-like array (tableaux de taille fixe)
  • Les tuple-like (std::array, std::tuple, std::pair)
  • Les classes/structures ayant toutes leurs variables membres publiques

Si une classe/structure contient des variables membre privées, il n’est pas possible de les ignorer dans une structured binding declaration.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct Person
{
	Person(std::string firstName, std::string lastName, int birthYear):
		firstName{std::move(firstName)},
		lastName{std::move(lastName)},
		birthYear{birthYear}
	{}

    std::string firstName;
	std::string lastName;
private:
    std::size_t age = 3;
};

auto main() -> int
{
    auto person = Person{};
    auto [firstName, lastName] = person; // error: type 'Person' decomposes into 3 elements, but only 2 names were provided
	auto [firstName, lastName, age] = person; // error: cannot decompose private member 'age' of 'Person'
}

Cette structure Person ne répond plus aux exigences pour être déstructurable (qui est “avoir toutes ses variables membres publiques”).
Mais il est possible de transformer cette structure pour qu’elle puisse satisfaire les critères d’un tuple-like.
Elle en deviendrait déstructurable.

Pour cela il faut la rendre compatible avec std::get. Ce qui implique d’ajouter:

  • Une spécialisation de std::tuple_size
  • Une spécialisation de std::tuple_element
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
struct Person
{
	Person(std::string firstName, std::string lastName, int birthYear):
		firstName{std::move(firstName)},
		lastName{std::move(lastName)},
		birthYear{birthYear}
	{}

	// Fonction membre pour accéder aux variables membres depuis la spécialisation de std::tuple_element<I, Person>
	template<std::size_t I, class T>
	constexpr auto&& get(this T&& self) noexcept
	{
		if constexpr (I == 0)
			return std::forward<T>(self).firstName;
		else if constexpr (I == 1)
			return std::forward<T>(self).lastName;
		else if constexpr (I == 2)
			return std::forward<T>(self).birthYear;
	}

    std::string firstName;
	std::string lastName;
private:
	int birthYear;
};

// Spécialisation de std::tuple_size pour le type Person. Pour préciser qu'il contient 3 éléments.
template <>
struct std::tuple_size<Person>: std::integral_constant<std::size_t, 3>
{};

// Spécialisation de std::tuple_element pour le type Person. Pour accéder aux éléments.
template <std::size_t I>
struct std::tuple_element<I, Person>
{
    using type = std::remove_cvref_t<decltype(std::declval<Person>().get<I>())>;
};

auto main() -> int
{
	auto person = Person{"Bjarne", "Stroustrup", 1950};
	const auto& [firstName, lastName, birthYear] = person;
	std::print("{} {} est né en {}\n", firstName, lastName, birthYear);
}

Si la classe/structure contenait d’autres variables publiques ou privées, elles ne seraient pas récupérables avec la structured binding declaration tant qu’elles ne sont pas supportées par ces éléments que nous venons d’ajouter.

constexpr Structured Binding (depuis C++26)

Avant C++26, les structured binding declaration ne peuvent pas être constexpr:

constexpr auto [x, y] = std::pair{1, 2}; // error: structured binding declaration cannot be 'constexpr'

Depuis C++26 (proposal, approval), les structured binding declaration supportent constexpr.
Ce n’est cependant pas encore supporté par les compilateurs à l’heure où j’écris.

Attributs individuels (depuis C++26)

Les structured binding declaration ne supportent pas les attributs individuels avant C++26:

int i = 0, j [[maybe_unused]] = 0; // Ok, individual attributes
auto [k, l [[maybe_unused]] ] = std::pair{1, 2}; // warning: an attribute specifier sequence attached to a structured binding declaration is a C++2c extension [-Wc++26-extensions]
[[maybe_unused]] auto [x, y] = std::pair{1, 2}; // Ok

A noter que le compilateur se plaint d’une variable non utilisée seulement lorsque toutes les variables d’un structured binding declaration sont inutilisées.

auto [x, y] = std::pair{1, 2}; // warning: unused variable '[x, y]' [-Wunused-variable]

Si on utilise au moins une des variables, la structured binding declaration devient pertinente pour extraire la ou les valeurs utiles, donc cet avertissement disparait.

auto [x, y] = std::pair{1, 2}; // Ok
auto n = x; // warning: unused variable 'n' [-Wunused-variable]

Non autorisé dans les conditions

Les structured binding declaration ne sont pas autorisées dans les conditions:

if (auto [x, y] = std::pair{1, 2}) {} // warning: ISO C++17 does not permit structured binding declaration in a condition [-Wbinding-in-condition]

Ce qui est assez normal étant donné qu’on ne sait pas trop ce qui serait vérifié par cette condition.

Mais elles autorisées dans la partie initialisation (init-statement (C++17)) des conditions:

if (auto [x, y] = std::pair{1, 2}; x == y) {}  // Ok

auto in template parameters (depuis C++17)

Si vous n’êtes pas familiers avec les templates, passez faire un tour ici.

Vous avez surement remarqué que certains templates prennent des valeurs, au lieu de prendre des types.

Par exemple:

auto array = std::array<int, 3>{ 1, 2, 3 };

Ici, on instancie un std::array contenant 3 éléments de type int. Le nombre d’éléments est renseigné en template.

Le passage de valeur en template est possible en écrivant le type accepté à la place de typename/class dans la template (template<std::size_t>):

template<std::size_t value>
constexpr auto constant = value;
constexpr auto const IntConstant42 = constant<42>;

Pour plus de généricité, il est également possible de le définir avec auto (template<auto>). Ici, auto sert à indiquer une valeur en template qui sera déduite à l’instantiation.

template<auto value>
constexpr auto constant = value;
constexpr auto const IntConstant42 = constant<42>;

Equivaut à:

template<class Type, Type value>
constexpr Type constant = value;
constexpr auto const IntConstant42 = constant<int, 42>;

template<auto> accepte toute constant expression, c’est à dire toute valeur connue à la compilation (integral, pointer, pointer to member, enum, lambda, constexpr object).

template<auto> ne supporte pas le type double avant C++20.

Utilisé dans une variadic, chaque valeur passée en template peut avoir son propre type:

template<auto... vs>
struct HeterogenousValueList {};
using MyList = HeterogenousValueList<42, 'X', 13u>;

AA (Always Auto) (depuis C++17)

En C++17, le langage garanti la copy elision, faisant disparaitre les surcoûts que nous avons vu à la fin de la partie sur “Amost Always Auto”, rendant l’utilisation de auto possible même sur des types qui ne sont ni copyables, ni movables.

La copy elision est une optimisation qui élimine la création et la copie d’objets temporaires (prvalue). Au lieu de créer une copie intermédiaire, l’objet est directement construit à l’emplacement final.

Suite à ce changement dans le langage, Herb Sutter soutient le passage de AAA à AA.

A votre tour de prendre le pas et d’adopter auto dans vos projets.

Abbreviated function template (depuis C++20)

Les templates ont toujours été très verbeuses.

template<class Lhs, class Rhs>
auto sum(Lhs lhs, Rhs rhs) -> auto
{
	return lhs + rhs;
};

Depuis C++20, il est possible d’utiliser auto comme syntaxe alternative aux templates, améliorant grandement leur lisibilité:

auto sum(auto lhs, auto rhs) -> auto
{
	return lhs + rhs;
};

Attention, derrière ses airs de placeholder type specifiers, il s’agit ici bien de types templatés.
Une template n’est pas toujours souhaitable. Dans cette situation il faut n’utiliser auto que si une template est souhaitée.

Notez aussi que les deux paramètres de auto sum(auto lhs, auto rhs) -> auto auront chacun leur propre type template.
Ils ne partageront pas un type template commun.
Ca équivaut à template<class T1, class T2> auto sum(T1 lhs, T2 rhs) -> auto
Pas à: template<class T> auto sum(T lhs, T rhs) -> auto

Comme avec les templates, il est toujours possible de faire des variadic template avec auto:

auto sum(auto... types) -> auto
{
	return (types + ...);
};

Lorsque templates et paramètres auto sont combinés, cela équivaut à avoir les types des paramètres auto après les templates:

template<class Lhs>
void sum(Lhs lhs, auto rhs);

Equivaut à:

template<class Lhs, class Rhs>
void sum(Lhs lhs, Rhs rhs);

auto cast (depuis C++23)

Une manière générique d’obtenir la copie d’un objet en C++ est auto variable = x;, mais une telle copie est une lvalue.

auto(a) (ou auto{x}) permet d’en obtenir une copie sous forme de prvalue, ce qui peut être utile pour transmettre cet objet en paramètre à une fonction.

function(auto(expr));
function(auto{expr});

Structured binding pack (depuis C++26)

Work in progress

Aller plus loin: